有丶抽象,学到自闭

参考的文章:

zcysky:【学习笔记】dsu on tree

Arpa:[Tutorial] Sack (dsu on tree)


先康一康模板题吧:CF 600E($Lomsat$ $gelral$)

虽然已经用莫队搞过一遍了(可以参考之前写的博客~),但这个还是差距挺大

我们如果对于每个节点暴力统计答案,是$O(N^2)$的复杂度:最坏情况下整棵树是一条链,对于每个节点的统计平均下来是$O(N)$的

具体是怎么做的呢?

对于以当前节点$x$为根的子树,我们建立$cnt$和$sum$两个数组(其实只要$sum$数组就够用啦)

$cnt[i]$:颜色$i$在子树中出现的次数

$sum[i]$:在子树中出现次数为$i$的颜色,其颜色的序号之和

我们还可以建立一个指针$top$,表示出现次数最多的颜色出现了多少次,在改变$cnt$数组的时候可以顺便维护下$top$

那么,对于这个子树,我们只要跑一边$dfs$,把所有后代全部统计一波,最后的结果就是$ans[x]=sum[top]$

现在我们希望能够降低对于每个节点统计的复杂度

$dsu$ $on$ $tree$是$O(N\cdot logN)$的做法,需要用到一些树剖的知识

在这道题目中,拿到了这颗树的连边,我们先用树剖怼上去

不用太着急,只要进行第一个$dfs$、得到$son$数组(即每个节点的重儿子)就够了

接下来的蛇皮操作需要理解一下

对于以节点$x$为根的子树,我们这样计算其结果$ans[x]$:

  1. 将$x$的儿子分成两种,一种是重儿子,另一种是轻儿子
  2. 我们先按照最上面方法的递归计算所有轻儿子的结果,计算完以后,不对$cnt$、$sum$、$top$进行任何保留(保留与否的操作在下一层递归的第$5$步实现)
  3. 我们再递归计算重儿子的的结果,但是计算完后,保留计算重儿子答案时的$cnt$、$sum$、$top$
  4. 结束递归、回到当前节点$x$这层以后,由于保留了计算重儿子时的统计信息,我们此时对重儿子及其子树的信息是完全清楚的,但是我们依然不清楚轻儿子和它们的子树的信息,所以我们再递归的$dfs$一遍轻儿子,将信息放进计算重儿子的数组;这时,我们得到的$cnt$、$sum$、$top$与暴力统计得到的是一模一样的
  5. 但是节点$x$不一定是其父节点$fa[x]$的重儿子!如果不是,那么$dfs$一遍$x$将所有信息清空;否则就保留(这里就是第$2$、$3$步中的保留/不保留的具体实现的位置)

我们在第$2$步仅仅是为了计算轻儿子的结果,且不想让其统计信息干扰我们需要的重儿子的统计信息

绕的地方在于,我们在第$2$步的“不保留”实际上是在计算某个轻儿子后,将以这个轻儿子为根的子树信息全部从$cnt$、$sum$、$top$中抹去(即是在轻儿子的第$5$步实现,并不是在$x$的第$2$步) ←我一开始因为没理解这个自闭了好久

看上去挺暴力的...分析一下为什么是$O(N\cdot logN)$

对于每个节点$x$,它仅可能被 以其祖先为根的子树 统计,所以它被统计的次数与其到根节点的路径长度相关

但是由于我们将重节点的统计信息保留,所以对于每条重链,只会真正意义上$dfs$到$x$一次:在重链的底端;重链上的其余节点可以通过被保留的统计信息了解$x$的情况、不需要$dfs$

综上,$x$被统计的次数即为其到根节点的路径上链的个数,是$O(logN)$级别的

用这个思路写出的代码如下:

  1. #include <cstdio>
  2. #include <cstring>
  3. #include <cmath>
  4. #include <vector>
  5. using namespace std;
  6.  
  7. typedef long long ll;
  8. const int N=;
  9.  
  10. int n;
  11. int c[N];
  12. vector<int> v[N];
  13.  
  14. int fa[N],sz[N],son[N];
  15.  
  16. inline void dfs(int x,int f)
  17. {
  18. fa[x]=f;
  19. sz[x]=;
  20.  
  21. for(int i=;i<v[x].size();i++)
  22. {
  23. int next=v[x][i];
  24. if(next==fa[x])
  25. continue;
  26.  
  27. dfs(next,x);
  28. sz[x]+=sz[next];
  29. if(!son[x] || sz[son[x]]<sz[next])
  30. son[x]=next;
  31. }
  32. }
  33.  
  34. int top,cnt[N];
  35. ll sum[N],ans[N];
  36.  
  37. inline void Add(int x,int num)
  38. {
  39. sum[cnt[c[x]]]-=(ll)c[x];
  40. cnt[c[x]]+=num;
  41. sum[cnt[c[x]]]+=(ll)c[x];
  42. if(sum[top+])
  43. top++;
  44. if(!sum[top])
  45. top--;
  46.  
  47. for(int i=;i<v[x].size();i++)
  48. {
  49. int next=v[x][i];
  50. if(next==fa[x])
  51. continue;
  52. Add(next,num);
  53. }
  54. }
  55.  
  56. inline void Solve(int x,int keep)
  57. {
  58. for(int i=;i<v[x].size();i++)
  59. {
  60. int next=v[x][i];
  61. if(next==fa[x] || next==son[x])
  62. continue;
  63. Solve(next,);
  64. }
  65.  
  66. if(son[x])
  67. Solve(son[x],);
  68.  
  69. for(int i=;i<v[x].size();i++)
  70. {
  71. int next=v[x][i];
  72. if(next==fa[x] || next==son[x])
  73. continue;
  74. Add(next,);
  75. }
  76.  
  77. sum[cnt[c[x]]]-=(ll)c[x];
  78. cnt[c[x]]++;
  79. sum[cnt[c[x]]]+=(ll)c[x];
  80. if(sum[top+])
  81. top++;
  82. ans[x]=sum[top];
  83.  
  84. if(!keep)
  85. Add(x,-);
  86. }
  87.  
  88. int main()
  89. {
  90. // freopen("input.txt","r",stdin);
  91. scanf("%d",&n);
  92. for(int i=;i<=n;i++)
  93. scanf("%d",&c[i]);
  94. for(int i=;i<n;i++)
  95. {
  96. int x,y;
  97. scanf("%d%d",&x,&y);
  98. v[x].push_back(y);
  99. v[y].push_back(x);
  100. }
  101.  
  102. dfs(,);
  103. Solve(,);
  104.  
  105. for(int i=;i<=n;i++)
  106. printf("%lld ",ans[i]);
  107. return ;
  108. }

再整一道题:CF 1009F($Dominant$ $Indices$)

这题做完后,感觉这种玩法比莫队还暴力...

如果想写暴力的话,可以对于每个节点$x$遍历子树,用$cnt$数组统计各深度的节点一共有多少个、并不断更新答案深度$val$

由于我们是一个个统计节点的,所以正确的$val$一定能够在统计的时候被更新出来:如果$cnt[dep[x]]>cnt[val]$或者$cnt[dep[x]]==cnt[val]$ && $dep[x]<val$,那么$val=dep[x]$

如果想删除一个节点怎么办?是不是要用set什么的...

不需要!在树上启发式合并中,唯一的删除操作存在于不保留节点信息时删空子树,所以在得到答案的过程中,我们只会加入节点、不会删除节点,即相当于每次得到的信息跟暴力得到的是完全相同的

相比而言,莫队还得考虑删除是否为$O(1)$呢

  1. #include <cstdio>
  2. #include <cstring>
  3. #include <cmath>
  4. #include <vector>
  5. using namespace std;
  6.  
  7. const int N=;
  8.  
  9. int n;
  10. vector<int> v[N];
  11.  
  12. int fa[N],sz[N],dep[N],son[N];
  13.  
  14. inline void dfs(int x,int f)
  15. {
  16. fa[x]=f;
  17. sz[x]=;
  18. dep[x]=dep[fa[x]]+;
  19.  
  20. for(int i=;i<v[x].size();i++)
  21. {
  22. int next=v[x][i];
  23. if(next==fa[x])
  24. continue;
  25.  
  26. dfs(next,x);
  27.  
  28. sz[x]+=sz[next];
  29. if(!son[x] || sz[next]>sz[son[x]])
  30. son[x]=next;
  31. }
  32. }
  33.  
  34. int ans[N],cnt[N],val;
  35.  
  36. inline void Add(int x,int num)
  37. {
  38. cnt[dep[x]]+=num;
  39. val=(cnt[dep[x]]>cnt[val] || (cnt[dep[x]]==cnt[val] && dep[x]<val)?dep[x]:val);
  40.  
  41. for(int i=;i<v[x].size();i++)
  42. {
  43. int next=v[x][i];
  44. if(next==fa[x])
  45. continue;
  46. Add(next,num);
  47. }
  48. }
  49.  
  50. inline void Solve(int x,int keep)
  51. {
  52. for(int i=;i<v[x].size();i++)
  53. {
  54. int next=v[x][i];
  55. if(next==fa[x] || next==son[x])
  56. continue;
  57. Solve(next,);
  58. }
  59.  
  60. if(son[x])
  61. Solve(son[x],);
  62.  
  63. for(int i=;i<v[x].size();i++)
  64. {
  65. int next=v[x][i];
  66. if(next==fa[x] || next==son[x])
  67. continue;
  68. Add(next,);
  69. }
  70.  
  71. cnt[dep[x]]+=;
  72. val=(cnt[dep[x]]>cnt[val] || (cnt[dep[x]]==cnt[val] && dep[x]<val)?dep[x]:val);
  73. ans[x]=val;
  74.  
  75. if(!keep)
  76. Add(x,-);
  77. }
  78.  
  79. int main()
  80. {
  81. // freopen("input.txt","r",stdin);
  82. scanf("%d",&n);
  83. for(int i=;i<n;i++)
  84. {
  85. int x,y;
  86. scanf("%d%d",&x,&y);
  87. v[x].push_back(y);
  88. v[y].push_back(x);
  89. }
  90.  
  91. dfs(,);
  92. Solve(,);
  93.  
  94. for(int i=;i<=n;i++)
  95. printf("%d\n",ans[i]-dep[i]);
  96. return ;
  97. }

一道被我做难的题:CF 375D($Tree$ $and$ $Queries$)

题意跟$Lomsat$ $gelral$有点像,但是求的是出现次数不少于$k$的颜色有多少种

如果统计出现次数$cnt$,那么我们可以在当前节点$x$合并完子树后,在$cnt$数组上求区间和,也就是把第一题中的$sum$数组上的更新搬到线段树上

不过有种更妙的方法,用类似栈的思想,只需要开一个数组来统计:

  • 如果颜色$color$被加入,那么出现次数不少于$cnt[color]$的颜色数不变(因为$color$已经在之前被统计过了),次数不少于$cnt[color]+1$的颜色数增加$1$,最后$cnt[color]++$
  • 如果颜色$color$被删去,那么出现次数不少于$cnt[color]$的颜色数减$1$,次数不少于$cnt[color]-1$的颜色数不变,最后$cnt[color]--$

不过如果最后询问的是出现次数不少于$k$的颜色数量$\times$颜色序号,应该是没法避免线段树了

还是应该再多想想的...

(这题好像莫队也能玩的很开心,没反应过来)

  1. #include <cstdio>
  2. #include <cstring>
  3. #include <cmath>
  4. #include <vector>
  5. using namespace std;
  6.  
  7. typedef pair<int,int> pii;
  8. const int N=;
  9.  
  10. int SZ=;
  11. int t[N<<];
  12.  
  13. inline void Modify(int k,int x)
  14. {
  15. k=k+SZ;
  16. t[k]+=x;
  17. k>>=;
  18.  
  19. while(k)
  20. {
  21. t[k]=(t[k<<]+t[k<<|]);
  22. k>>=;
  23. }
  24. }
  25.  
  26. inline int Query(int k,int p,int a,int b)
  27. {
  28. if(b<p)
  29. return ;
  30. if(a>=p)
  31. return t[k];
  32.  
  33. int mid=(a+b)>>;
  34. return Query(k<<,p,a,mid)+Query(k<<|,p,mid+,b);
  35. }
  36.  
  37. int n,m;
  38. int c[N];
  39. vector<int> v[N];
  40.  
  41. int fa[N],sz[N],son[N];
  42.  
  43. inline void dfs(int x,int f)
  44. {
  45. fa[x]=f;
  46. sz[x]=;
  47.  
  48. for(int i=;i<v[x].size();i++)
  49. {
  50. int next=v[x][i];
  51. if(next==fa[x])
  52. continue;
  53.  
  54. dfs(next,x);
  55. sz[x]+=sz[next];
  56. if(!son[x] || sz[next]>sz[son[x]])
  57. son[x]=next;
  58. }
  59. }
  60.  
  61. vector<pii> q[N];
  62. int ans[N],cnt[N];
  63.  
  64. inline void Add(int x,int num)
  65. {
  66. Modify(cnt[c[x]],-);
  67. cnt[c[x]]+=num;
  68. Modify(cnt[c[x]],);
  69.  
  70. for(int i=;i<v[x].size();i++)
  71. {
  72. int next=v[x][i];
  73. if(next==fa[x])
  74. continue;
  75. Add(next,num);
  76. }
  77. }
  78.  
  79. inline void Solve(int x,int keep)
  80. {
  81. for(int i=;i<v[x].size();i++)
  82. {
  83. int next=v[x][i];
  84. if(next==fa[x] || next==son[x])
  85. continue;
  86. Solve(next,);
  87. }
  88.  
  89. if(son[x])
  90. Solve(son[x],);
  91.  
  92. for(int i=;i<v[x].size();i++)
  93. {
  94. int next=v[x][i];
  95. if(next==fa[x] || next==son[x])
  96. continue;
  97. Add(next,);
  98. }
  99.  
  100. Modify(cnt[c[x]],-);
  101. cnt[c[x]]++;
  102. Modify(cnt[c[x]],);
  103.  
  104. for(int i=;i<q[x].size();i++)
  105. {
  106. int rnk=q[x][i].first,id=q[x][i].second;
  107. ans[id]=Query(,rnk,,SZ-);
  108. }
  109.  
  110. if(!keep)
  111. Add(x,-);
  112. }
  113.  
  114. int main()
  115. {
  116. // freopen("input.txt","r",stdin);
  117. scanf("%d%d",&n,&m);
  118. while(SZ<n+)
  119. SZ<<=;
  120. for(int i=;i<=n;i++)
  121. scanf("%d",&c[i]);
  122. for(int i=;i<n;i++)
  123. {
  124. int x,y;
  125. scanf("%d%d",&x,&y);
  126. v[x].push_back(y);
  127. v[y].push_back(x);
  128. }
  129.  
  130. dfs(,);
  131.  
  132. for(int i=;i<=m;i++)
  133. {
  134. int x,y;
  135. scanf("%d%d",&x,&y);
  136. q[x].push_back(pii(y,i));
  137. }
  138.  
  139. Solve(,);
  140.  
  141. for(int i=;i<=m;i++)
  142. printf("%d\n",ans[i]);
  143. return ;
  144. }

也是一种树上统计的技巧吧

以后遇到题目再补在这里

Luogu P1600 (天天爱跑步,$NOIP2016$)

题解写在树上差分里面了

CF Gym 259514K  ($Tree$,$2019ICPC$南昌)

有点类似树上背包的思想,在solve轻儿子时也将信息合并到当前节点上去

至于其他的就是pbds NB

  1. #include <cstdio>
  2. #include <vector>
  3. #include <cstring>
  4. #include <algorithm>
  5. using namespace std;
  6.  
  7. #include <ext/pb_ds/tree_policy.hpp>
  8. #include <ext/pb_ds/assoc_container.hpp>
  9. using namespace __gnu_pbds;
  10.  
  11. typedef long long ll;
  12. typedef pair<int,int> pii;
  13. typedef tree<pii,null_type,less<pii>,rb_tree_tag,tree_order_statistics_node_update> rbtree;
  14.  
  15. const int INF=<<;
  16. const int N=;
  17.  
  18. int n,k;
  19. int val[N];
  20. vector<int> v[N];
  21.  
  22. int dep[N],sz[N],son[N];
  23.  
  24. void dfs(int x,int fa)
  25. {
  26. dep[x]=dep[fa]+;
  27. sz[x]=;
  28.  
  29. for(int i=;i<v[x].size();i++)
  30. {
  31. int nxt=v[x][i];
  32. dfs(nxt,x);
  33.  
  34. sz[x]+=sz[nxt];
  35. if(!son[x] || sz[nxt]>sz[son[x]])
  36. son[x]=nxt;
  37. }
  38. }
  39.  
  40. ll ans;
  41. rbtree t[N];
  42.  
  43. void add(int x,int dlt)
  44. {
  45. if(dlt)
  46. t[val[x]].insert(pii(dep[x],x));
  47. else
  48. t[val[x]].erase(pii(dep[x],x));
  49.  
  50. for(int i=;i<v[x].size();i++)
  51. add(v[x][i],dlt);
  52. }
  53.  
  54. void calc(int x,int lca)
  55. {
  56. int rem=val[lca]*-val[x];
  57. if(rem>=)
  58. {
  59. int ord=t[rem].order_of_key(pii(*dep[lca]-dep[x]+k,INF));
  60. ans+=ord;
  61. }
  62.  
  63. for(int i=;i<v[x].size();i++)
  64. calc(v[x][i],lca);
  65. }
  66.  
  67. void solve(int x,int lca,int keep)
  68. {
  69. for(int i=;i<v[x].size();i++)
  70. {
  71. int nxt=v[x][i];
  72. if(nxt!=son[x])
  73. solve(nxt,nxt,);
  74. }
  75.  
  76. if(son[x])
  77. solve(son[x],son[x],);
  78.  
  79. for(int i=;i<v[x].size();i++)
  80. {
  81. int nxt=v[x][i];
  82. if(nxt!=son[x])
  83. {
  84. calc(nxt,lca);
  85. add(nxt,);
  86. }
  87. }
  88.  
  89. t[val[x]].insert(pii(dep[x],x));
  90. if(!keep)
  91. add(x,);
  92. }
  93.  
  94. int main()
  95. {
  96. scanf("%d%d",&n,&k);
  97. for(int i=;i<=n;i++)
  98. scanf("%d",&val[i]);
  99. for(int i=;i<=n;i++)
  100. {
  101. int x;
  102. scanf("%d",&x);
  103. v[x].push_back(i);
  104. }
  105.  
  106. dfs(,);
  107. solve(,,);
  108.  
  109. printf("%lld\n",ans*);
  110. return ;
  111. }

(完)

树上启发式合并(dsu on tree)学习笔记的更多相关文章

  1. 神奇的树上启发式合并 (dsu on tree)

    参考资料 https://www.cnblogs.com/zhoushuyu/p/9069164.html https://www.cnblogs.com/candy99/p/dsuontree.ht ...

  2. 树上启发式合并 (dsu on tree)

    这个故事告诉我们,在做一个辣鸡出题人的比赛之前,最好先看看他发明了什么新姿势= =居然直接出了道裸题 参考链接: http://codeforces.com/blog/entry/44351(原文) ...

  3. dsu on tree学习笔记

    前言 一次模拟赛的\(T3\):传送门 只会\(O(n^2)\)的我就\(gg\)了,并且对于题解提供的\(\text{dsu on tree}\)的做法一脸懵逼. 看网上的其他大佬写的笔记,我自己画 ...

  4. dsu on tree 学习笔记

    这是一个黑科技,考虑树链剖分后,每个点只会在轻重链之间转化\(log\)次. 考虑暴力是怎么写的,每次枚举一个点,再暴力把子树全部扫一边. \(dsu\ on\ tree.\)的思想就是保留重儿子不清 ...

  5. 【CF600E】Lomset gelral 题解(树上启发式合并)

    题目链接 题目大意:给出一颗含有$n$个结点的树,每个节点有一个颜色.求树中每个子树最多的颜色的编号和. ------------------------- 树上启发式合并(dsu on tree). ...

  6. dsu on tree 树上启发式合并 学习笔记

    近几天跟着dreagonm大佬学习了\(dsu\ on\ tree\),来总结一下: \(dsu\ on\ tree\),也就是树上启发式合并,是用来处理一类离线的树上询问问题(比如子树内的颜色种数) ...

  7. dsu on tree (树上启发式合并) 详解

    一直都没出过算法详解,昨天心血来潮想写一篇,于是 dsu on tree 它来了 1.前置技能 1.链式前向星(vector 建图) 2.dfs 建树 3.剖分轻重链,轻重儿子 重儿子 一个结点的所有 ...

  8. 【Luogu U41492】树上数颜色——树上启发式合并(dsu on tree)

    (这题在洛谷主站居然搜不到--还是在百度上偶然看到的) 题目描述 给一棵根为1的树,每次询问子树颜色种类数 输入输出格式 输入格式: 第一行一个整数n,表示树的结点数 接下来n-1行,每行一条边 接下 ...

  9. 【学习笔记/题解】树上启发式合并/CF600E Lomsat gelral

    题目戳我 \(\text{Solution:}\) 树上启发式合并,是对普通暴力的一种优化. 考虑本题,最暴力的做法显然是暴力统计每一次的子树,为了避免其他子树影响,每次统计完子树都需要清空其信息. ...

随机推荐

  1. CSS 3D的应用记录

    为父元素添加以下样式后,子元素即可使用3D属性,例如translateZ /*设置子元素也应用3D效果*/-webkit-transform-style: preserve-3d;-moz-trans ...

  2. 170616、解决 java.lang.IllegalArgumentException: No converter found for return value of type: class java.util.ArrayList

    报错截图: 原因:搭建项目的时候,springmvc默认是没有对象转换成json的转换器的,需要手动添加jackson依赖. 解决步骤: 1.添加jackson依赖到pom.xml <!-- j ...

  3. Android官方架构组件介绍之ViewModel

    ViewModel 像Activity,Fragment这类应用组件都有自己的生命周期并且是被Android的Framework所管理的.Framework可能会根据用户的一些操作和设备的状态对Act ...

  4. Uva10917 Walk Through the Forest

    题目链接:https://vjudge.net/problem/UVA-10917 题目意思:Jimmy下班回家要闯过一下森林,劳累一天后在森林中散步是非常惬意的事,所以他打算每天沿着一条不同的路径回 ...

  5. Grafana+Prometheus监控

    添加模板一定要看说明以及依赖 监控redis https://blog.52itstyle.com/archives/2049/ http://www.cnblogs.com/sfnz/p/65669 ...

  6. 前端页面汉子显示为问号,需修改 linux下面修改mysql 数据库的字符编码为utf8

    设置MySQL数据库编码为UTF-8 登陆后查看数据库当前编码:SHOW VARIABLES LIKE 'char%'; 修改/etc/mysql/my.cnf (默认安装路径下) (标签下没有的添加 ...

  7. CentOS7.5基础优化与常用配置

    目录 最小化全新安装CentOS7基础优化 配置yum源 安装常用软件 关闭防火墙 关闭SELinux 优化ulimit 历史命令记录改为1万条 把命令提示符改为绿色 添加vim配置文件 添加一个普通 ...

  8. Mybatis一对一映射

    一.Mybatis一对一映射 本例讲述使用mybatis开发过程中常见的一对一映射查询案例.只抽取关键代码和mapper文件中的关键sql和配置,详细的工程搭建和Mybatis详细的流程代码可参见&l ...

  9. C++中的常量定义

    本篇笔记总结自一次代码检视. 一般来说,使用C语言编程时我们都习惯在代码当中使用C当中的宏定义来定义一个数值常量: #define MY_CONST 7 在C++开发项目时,也会经常存在沿袭C当中常量 ...

  10. java之throw和throws

    抛出异常有三种形式,一是throw,一个throws,还有一种系统自动抛异常.下面它们之间的异同. 一.系统自动抛异常 当程序语句出现一些逻辑错误.主义错误或类型转换错误时,系统会自动抛出异常:(举个 ...