dsu on tree
树上启发式合并
dsu on tree 主要可以解决一些静态的子树信息查询的问题,当然也可以拓展到路径,最常见的套路就是强制该路径经过当前子树根节点。
启发式算法是基于人类的经验和直观感觉,对一些算法的优化。——来自 OI Wiki
e.g. 并查集启发式合并
并查集的按秩合并:将深度小的并查集合并到深度大的并查集。
code
void merge(int x, int y) {
int xx = find(x), yy = find(y);
if (size[xx] < size[yy]) swap(xx, yy);
fa[yy] = xx;
size[xx] += size[yy];
}
——又来自 OI Wiki
那么什么是 dsu on tree 呢?
例题
给出一棵 \(n\) 个节点以 \(1\) 为根的树,节点 \(u\) 的颜色为 \(c_u\) ,有 \(m\) 次询问,每次给出结点 \(u\) 询问 \(u\) 子树里一共出现了多少种不同的颜色。
对于所有数据,有 \(1\le m,c_i\le n\le 10^5\)
暴力算法 \(O(nm)\)
每次询问遍历 \(u\) 的整棵子树。
树上莫队 \(O(n\sqrt n)\)
emm... 等我学完莫队补充。
dsu on tree \(O(nlogn)\)
今天的正题来啦~!
考虑离线询问,预处理出答案后 \(O(1)\) 回答每个询问。
\(u\) 的信息可以通过他的儿子转移,但是空间不允许储存每个儿子的信息。
因此我们只用桶存储重儿子(在本题为 \(size\) 最大的儿子)的信息,然后暴力地遍历轻儿子的子树,将答案加到桶中。
Code
#include<bits/stdc++.h>
#define ll long long
#define pf printf
#define sf scanf
using namespace std;
const int N=1e5+7;
int n,c[N];
int m,u;
int v;
int sum[N];//每棵子树的不同颜色数
vector<int> son[N];
int siz[N],cnt[N];// cnt为桶
int gson[N];// 重儿子
int tot;// 当前不同颜色数
void dfs(int u,int fa){
siz[u]=1;
for(int v:son[u]){
if(v==fa) continue;
siz[u]+=siz[v];
if(siz[v]>siz[gson[u]]) gson[u]=v;
}
}
void add(int c) {if(!cnt[c]++) tot++;}
void del(int c) {if(!--cnt[c]) tot--;}
void change(int u,int fa,bool k){//k 表示加或减
if(k) add(c[u]);
else del(c[u]);
for(int v:son[u]){
if(v==fa) continue;
change(v,u,k);
}
}
void dsu(int u,int fa,bool k){//k 表示是否保留桶的信息
for(int v:son[u]){//计算轻儿子答案,算完删除桶
if(v==fa||v==gson[u]) continue;
dsu(v,u,0);
}
if(gson[u]) dsu(gson[u],u,1);//计算重儿子答案,算完保留桶
add(c[u]);
for(int v:son[u]){//暴力遍历合并轻儿子答案
if(v==fa||v==gson[u]) continue;
change(v,u,1);
}
sum[u]=tot;//记录答案
if(!k) change(u,fa,0);//清空桶(memset会超时)
}
int main(){
sf("%d",&n);
for(int i=1;i<n;i++){
sf("%d%d",&u,&v);
son[u].push_back(v),son[v].push_back(u);
}
for(int i=1;i<=n;i++){
sf("%d",&c[i]);//颜色
}
dfs(1,0);//找重儿子,O(n)
dsu(1,0,0);
sf("%d",&m);
for(int i=1;i<=m;i++){
sf("%d",&u);
pf("%d\n",sum[u]);
}
}
经验
以上两题都可以用 dsu on tree 由处理子树问题扩展到处理路径问题解决。
具体做法参考题解。
我没写
众所周知,如果可以更改 \(a_i\),则所有经过 \(i\) 的路径权值 \(d\) 都一定不为 \(0\)(将 \(a_i\) 改为极大的数即可)。
考虑深搜解决,也就是说,处理 \(u\) 时,\(u\) 的子树已处理完毕。
对于 \(d(u,v)=0\),更改 \(\text{lca}(u,v)\) 一定是最优的,证明如下:
若 \((u,lca)\) 上有一点 \(k\),存在一条经过 \(k\) 权值为 \(0\) 的路径 \((l,r)\),其中 \(r\) 为 \(l\) 的祖先,那么 \((l,r)\) 必定经过 \(lca\),否则该路径已经处理过了,\(d(l,r)\neq 0\)。因此更改 \(lca\) 是最优的。
首先预处理出每个结点到根的路径权值,即 \(d_u\),方便后面计算。
然后我们对每个点开一个 set,储存他的子树内所有点到根节点的权值。
考虑启发式合并,每个结点继承 \(set_{size}\) 最大的儿子的 \(set\),然后暴力枚举合并其他儿子。
计算答案只需在 \(set\) 里使用 \(find\) 函数查询是否存在值 \(d_x \ xor\ a_u\),若存在,则点 \(u\) 需要更改,答案 \(+1\),同时清空 \(set_u\)。
P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并 线段树合并板子当然要用可爱的 dsu on tree 做啦因为维护最大值需要再开一个数据结构维护,我使用了常数较小的 priority_queue,时间比线段树合并多一个 \(\log\),是 \(O(n \log^2 n)\) 的,但是可能因为可爱的 dsu 的优秀常数应该说是因为人见人爱花见花开的线段树的巨大常数导致在没有优化常数的情况下甚至也可以吊打线段树合并。
本文来自博客园,作者:liyixin,转载请注明原文链接:https://www.cnblogs.com/liyixin0514/p/18357742