• 洛谷 P5306 [COCI2019] Transport 题解


    题目大意:

    在一棵树中,找出满足条件的点对 ij 数量,使从 i 能走到 j,其中任意时刻,满足点权之和大于等于边权之和。

    算法分析:

    看题目的描述,很明显是一道点分治的题目。关键在于如何去维护答案。

    这道题是我们的模拟赛题,考场上打了一个假到不能再假的点分治,一分没有。

    先来模拟一下点分治的过程:首先找到 root,对于根为 root 的这棵树进行操作,最后把root 删掉。

    那么这道题,很自然的难点,就是如何对这棵树进行操作,我们一步一步看。

    首先,i 和 j 要在 root 的两棵不同的子树中,最后统计答案的时候,把 ij 在一棵子树的情况给删掉就行。

    关键操作:把 i 到 j 的路径,分为 i 到 root 和 root 到 j。我们用两个数组 w_i​,d_i 分别记录这条路径的点权之和,和边权之和。

    第一步,从 i 到 root :对于满足条件的节点 k,需要 w[i]-w[k] \geq d[i]-d[k],把这个式子移项,可以得到 (w[i]-d[i])-(w[k]-d[k]) \geq 0。所以我们记录最大的 w[k]-d[k] 就可以判断 i 到 root 是否合法。

    第二步,从 root 到 j:这种情况,该路径的起点不一定是 root,可以从另一个子树的 u 为起点。设以 u 为起点到 root,剩余的油量是 x,则路径上的任意一点 k,需要满足 x+w[fa[k]] \geq d[k],fa[k] 表示 k 的父亲。变换一下,式子就变成 x+(w[fa[k]]-d[k]) \geq 0。这时我们维护路径上 w[fa[k]]-d[k] 的最小值即可。

    如何统计答案:

    先遍历所有子树,从 i 到 root 记录 w[i]-d[i] 表示从 i 到 root 的剩余油量。

    从 root 到 j 记录 w[fa[j]]-d[j] 的最小值。

    将两种情况按升序排序,用双指针来维护答案的区间。

    答案区间为 w[k]-d[k]+w[f[a[j]]-d[j] 的 k 都可以。

    注意,像上面所说,要先把 i 和 k 在同一棵子树中的情况提前删掉。 

    总代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #define re register
    7. #define int long long
    8. #define drep(a,b,c) for(re int a(b) ; a>=(c) ; --a)
    9. #define rep(a,b,c) for(re int a(b) ; a<=(c) ; ++a)
    10. using namespace std;
    11. inline int read(){
    12. int x=0,f=1;char ch=getchar();
    13. while(ch<'0'||ch>'9'){if(ch == '-') f=-1 ; ch=getchar();}
    14. while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
    15. return x*f;
    16. }
    17. const int M = 2e5+10;
    18. int n;
    19. int head[M],siz[M],maxp[M],vis[M],a[M];
    20. int w[M],d[M],tmp1[M],tmp2[M],q1[M],q2[M];
    21. int root,sum,ans,cnt,cnt1,cnt2;
    22. struct edge{
    23. int to,nxt,w;
    24. }e[M];
    25. inline void add(int u,int v,int w){ //村边
    26. e[++cnt].to = v;
    27. e[cnt].w = w;
    28. e[cnt].nxt = head[u];
    29. head[u] = cnt;
    30. }
    31. inline void getroot(int u,int fa){
    32. siz[u] = 1;maxp[u] = 0; //maxp记录该节点的最大的子树的size
    33. for(re int i(head[u]) ; i ; i=e[i].nxt){
    34. int v = e[i].to;
    35. if(vis[v] || v==fa) continue;
    36. getroot(v,u);
    37. siz[u] += siz[v];
    38. maxp[u] = max(maxp[u],siz[v]); //找最大
    39. }
    40. maxp[u] = max(maxp[u],sum-siz[u]); //注意,该节点外部也可能有点,要考虑到
    41. if(maxp[u] < maxp[root]) root = u; //更新重心
    42. }
    43. inline void dfs(int u,int fa,int mx,int mi){
    44. if(w[u] - d[u] >= mx) tmp1[++cnt1] = w[u]-d[u]; //把符合要求的记录下来
    45. tmp2[++cnt2] = mi; //因为不确定从root到j的路径是从哪一个点开始,所以也都要记录下来
    46. for(re int i(head[u]) ; i ; i=e[i].nxt){
    47. int v = e[i].to;
    48. if(v==fa || vis[v]) continue;
    49. d[v] = d[u]+e[i].w; //更新边权之和
    50. w[v] = w[u]+a[v]; //更新点权之和
    51. dfs(v,u,max(mx,w[u]-d[u]),min(mi,w[u]-d[v])); //往下接着更新
    52. }
    53. }
    54. inline void solve(int u){
    55. int tot1=0,tot2=0;
    56. d[u] = 0,w[u] = a[u]; //初始化
    57. for(re int i(head[u]) ; i ; i=e[i].nxt){
    58. int v = e[i].to;
    59. if(vis[v]) continue;
    60. cnt1 = cnt2 = 0;
    61. d[v] = e[i].w,w[v] = w[u]+a[v]; //初始化两个数组
    62. dfs(v,u,w[u]-d[u],w[u]-d[v]); //进行dfs,找tmp1和tmp2来更新答案
    63. sort(tmp1+1,tmp1+cnt1+1); //升序
    64. sort(tmp2+1,tmp2+cnt2+1); //tmp1和tmp2记录的是当前子树内的路径
    65. int l = 1;
    66. for(re int i(cnt2) ; i>=1 ; --i){ //注意是反着枚举,相当于双指针维护
    67. while(l<=cnt1 && tmp1[l]+tmp2[i]-a[u]<0) l++; //如果改路径不符合要求
    68. ans -= cnt1-l+1; //把在同一棵子树内的贡献给删掉
    69. }
    70. for(re int i(1) ; i<=cnt1 ; ++i) q1[++tot1] = tmp1[i];
    71. for(re int i(1) ; i<=cnt2 ; ++i) q2[++tot2] = tmp2[i]; //q1和q2记录的是所有的路径
    72. }
    73. sort(q1+1,q1+tot1+1); //同样升序排序
    74. sort(q2+1,q2+tot2+1);
    75. int l = 1;
    76. for(re int i(tot2) ; i>=1 ; --i){ //双指针找答案
    77. if(q2[i] >= 0) ans++; //相当于可以直接从root出发到j,那么直接+1即可
    78. while(l<=tot1 && q1[l]+q2[i]-a[u]<0) l++; //从i到root和从root到j加了两次a[root],减去就行
    79. ans += tot1-l+1; //更新答案
    80. }
    81. ans += tot1; //这是加上从i出发直接到root结束的情况
    82. }
    83. inline void work(int u){
    84. vis[u] = 1; //相当于把这个点删了
    85. solve(u); //从这个点开始
    86. for(re int i(head[u]) ; i ; i=e[i].nxt){
    87. int v = e[i].to;
    88. if(vis[v]) continue;
    89. maxp[root = 0] = sum = siz[v]; //重新找重心
    90. getroot(v,0);
    91. getroot(root,0); //注意还是两次
    92. work(root); //重复该过程
    93. }
    94. }
    95. signed main(){
    96. n=read();
    97. rep(i,1,n) a[i] = read();
    98. rep(i,1,n-1){
    99. int u=read(),v=read(),w=read();
    100. add(u,v,w),add(v,u,w);
    101. }
    102. maxp[0] = sum = n; //注意要先赋值
    103. getroot(1,0);
    104. getroot(root,0); //可以算是一个细节吧,因为如果只找一次根,
    105. //那么记录的siz数组很有可能不是以root为根的size
    106. work(root); //从root开始点分治
    107. printf("%lld\n",ans);
    108. return 0;
    109. }

  • 相关阅读:
    centos jenkins 无法启动 active (exited)
    Spring In Action 5 学习笔记 chapter1 拓展
    蓝桥算法赛(摆玩具)
    每日一题~组合总数III
    python多线程是如何工作
    select条目对象
    Java中String对象的replaceAll方法调用性能优化小技巧
    八股文随笔1
    Spring项目bean 无法注入问题--Thread中注入Bean无效-多线程下@Resource和@Autowired和@Value 注入为null
    第一百五十一回 自定义组件综合实例:游戏摇杆二
  • 原文地址:https://blog.csdn.net/glorious_dream/article/details/126254904