什么是DSU on tree ? 就是树上启发式合并,启发式合并就是两个集合,按照常规思路 我们是将小的集合并到大的集合里面去,依次合并所有的集合。可以证明这是logn 的时间复杂度。所谓树上启发式合并就是,在树上进行操作。
这里讲三个题分别是
首先第一题非常经典。
大意就是 有一颗树,统计 所有子树占主导的所有编号之和,所谓占主导就是,就是出现次数最多的那个编号。如果次数一样多,那么就都是众数。
我们如何思考,一般树上问题都是递归解决。
递归 最重要的是 终止条件 ,和本层需要做的工作,这两点做好,递归每一层都是一样的,
这里可以看看灵神的视频 看到递归就晕?带你理解递归的本质!
在了解递归之后,我们应该怎么做呢,暴力的思路就是,统计每个以某个节点为根的子树,暴力搜索所有节点,统计,然后在处理答案,这样时间复杂度是O(n²)的
优化思路:
在dsu on tree 中 我们每一个以某节点为根的树中,有若干儿子,一些儿子的包含的节点个数多 我们称之为重儿子,节点个数少的称为轻儿子。 根据启发式合并的思想,我们把轻儿子合并到重儿子中,在统计,所以 我们可以保留重儿子的信息,消去轻儿子的信息。 这样可以证明时间复杂度是O(nlogn) 的,可以去看看大佬证明。
总而言之,dsu on tree 格式大差不差,最重要的是 我们对不同的题目需要保存不同的信息。这点是不同的。最重要思考的是保存什么样的信息。根据题意来。
dsu 板子如下
首先是 dfs 函数 // 统计所需要的信息 ,如 dfs 序 重儿子 是哪个,等等
void dfs_init (int u,int f){
l[u] = ++tot;
id[tot] = u;
// id是存第几个序列是什么节点 l是存这个节点起始是第几序列
sz[u] = 1 ;
hs[u] = -1;
for (auto v : e[u]){
if(v==f)continue;
dfs_init(v,u);
sz[u] +=sz[v];
if(hs[u] == -1 || sz[v] > sz[hs[u]])
hs[u]=v;
}
r[u] = tot;
}
之后在进行一次 dfs 便是dsu on tree
void dfs_solve(int u ,int f,bool keep){
for(auto v :e[u]){
if ( v!= f && v!=hs[u]){
dfs_solve(v,u,false);
}
}
if(hs[u] != -1){
dfs_solve(hs[u],u,true);
// 重儿子的集合
}
auto add =[&](int x){
x = c[x];
cnt[x]++;
if(cnt[x] > maxcnt) maxcnt = cnt[x],sumcnt=0;
if (cnt[x] == maxcnt) sumcnt+=x;
};
auto del = [&](int x){
x = c[x];
cnt[x]--;
};
for (auto v : e[u]){
if(v!=f && v!= hs[u]){
for(int x =l[v];x<=r[v];x++)
add(id[x]);
}
}
// u本身加入
add(u);
ans[u] = sumcnt;
if(!keep){
maxcnt = 0;
sumcnt = 0;
for(int x = l[u];x<=r[u];x++){
del(id[x]);
}
}
}
注意 这第二次dfs 根据不同的题目进行修改 。
下面贴出完整代码。
# include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 101000;
int n;、
vector<int>e[N];
int l[N],r[N],id[N],sz[N],hs[N],tot,c[N];
int cnt[N];//每个颜色出现次数
int maxcnt;//众数出现次数
LL sumcnt,ans[N]; //众数的和
// dfs 序列
void dfs_init (int u,int f){
l[u] = ++tot;
id[tot] = u;
// id是存第几个序列是什么节点 l是存这个节点起始是第几序列
sz[u] = 1 ;
hs[u] = -1;
for (auto v : e[u]){
if(v==f)continue;
dfs_init(v,u);
sz[u] +=sz[v];
if(hs[u] == -1 || sz[v] > sz[hs[u]])
hs[u]=v;
}
r[u] = tot;
}
void dfs_solve(int u ,int f,bool keep){
for(auto v :e[u]){
if ( v!= f && v!=hs[u]){
dfs_solve(v,u,false);
}
}
if(hs[u] != -1){
dfs_solve(hs[u],u,true);
// 重儿子的集合
}
auto add =[&](int x){
x = c[x];
cnt[x]++;
if(cnt[x] > maxcnt) maxcnt = cnt[x],sumcnt=0;
if (cnt[x] == maxcnt) sumcnt+=x;
};
auto del = [&](int x){
x = c[x];
cnt[x]--;
};
for (auto v : e[u]){
if(v!=f && v!= hs[u]){
for(int x =l[v];x<=r[v];x++)
add(id[x]);
}
}
// u本身加入
add(u);
ans[u] = sumcnt;
if(!keep){
maxcnt = 0;
sumcnt = 0;
for(int x = l[u];x<=r[u];x++){
del(id[x]);
}
}
}
// dfs序是真的牛逼
int main(){
scanf("%d",&n);
for(int i = 1 ;i<=n;i++){
scanf("%d",&c[i]);
}
// 建树吧
for(int i = 1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
e[u].push_back(v);
e[v].push_back(u);
}
dfs_init(1,0);
dfs_solve(1,0,false);
for(int i = 1;i<=n;i++){
printf("%lld%c",ans[i]," \n"[i==n]);
}
}
那么第二题,我们应该如何思考呢?
题目是 给一棵树,每条边有权。求一条简单路径,权值和等于 k,且边的数量最小。
还是一样的,对于当前节点为根的树, 我们所需要的是 在保存重儿子之后, 依次暴力遍历轻儿子,因为 两点间的权值和 是 dfs(u) + dfs(v) - 2dfs(lca(u,v)) == k dfs(u) 这里定义为 根到u的权值 lca 是u和v 的最近公共祖先。 所以在遍历轻儿子时,我们查询是否存在这个权值 ,使得 d = k + 2dfs(lca(u,v)) - dfs(v) 存在 则比较边数是否跟已有的还要小,如果小,则更新全局变量,最后在所有节点遍历完后 输出答案即可。 下面是代码
# include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 200010;
int n,k;
vector<pair<int,int>> e[N];
int l[N],r[N],id[N],sz[N],hs[N],tot,c[N];
int dep1[N];
LL dep2[N];
int ans;
map<LL,int> val;
// 板子
void dfs_init(int u,int f){
l[u] = ++tot;
id[tot] = u;
sz[u] = 1;
hs[u] = -1;
for(auto[v,w] : e[u]){
if(v==f) continue;
dep1[v] = dep1[u] + 1;
dep2[v] = dep2[u] + w ;// dep2存的应该是权值
dfs_init(v,u);
sz[u] += sz[v];
if(hs[u] == -1 || sz[v] > sz[hs[u]])
hs[u] = v;
}
r[u] = tot;
}
void dfs_solve(int u,int f,bool keep){
for(auto [v,w] : e[u]){
//先处理轻儿子
if(v!=f && v!=hs[u]){
dfs_solve(v,u,false);
}
}
//处理重儿子
if(hs[u] != -1){
dfs_solve(hs[u],u,true);
}
// 编写查询函数
auto query = [&](int w){
LL d2 = k + 2*dep2[u] - dep2[w];
if (val.count(d2)){
ans = min(ans,val[d2]+dep1[w]-2*dep1[u]);
}
};
auto add = [&](int w){
if (val.count(dep2[w]))
val[dep2[w]] = min(val[dep2[w]],dep1[w]);
else
val[dep2[w]] = dep1[w];
};
//开始遍历轻儿子了
for(auto [v,w] : e[u]){
if (v != f && v!= hs[u]){
for(int x =l[v];x<=r[v];x++){
query(id[x]);
}
for(int x = l[v];x<=r[v];x++)
add(id[x]);
}
}
query(u);
add(u);
if(!keep){
val.clear();
}
}
// 递归的本质 就是分别处理每棵子树 保留重儿子,清除轻儿子, 最后一遍遍历轻儿子是处理 以u为根节点的子树哦
int main(){
scanf("%d%d",&n,&k);
for(int i = 1 ;i<n;i++){
int u ,v,w;
scanf("%d%d%d",&u,&v,&w);
++u;
++v;
// 他是0开始编号直接自己加1就行。
e[u].push_back({v,w});
e[v].push_back({u,w});
}
ans = n+ 1;
dfs_init(1,0);
dfs_solve(1,0,false);
if(ans >=n+1){
ans = -1;
}
printf("%d",ans);
return 0;
}
第三题 lqb 的题目 跟第一题几乎是一样的 ,板子题,我们只需要统计众数*众数的数目 是否等于以该点为根的节点个数 如果是 ans + 1 ,代码如下。
# include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 200010;
int n;
vector<int>e[N];
int cnt[N];//每个颜色出现次数
int maxcnt;//众数出现次数
LL sumcnt,ans; //众数的和 众数出现多少次
int l[N],r[N],id[N],sz[N],hs[N],tot,c[N];
void dfs(int u,int f){
l[u] = ++tot;
id[tot] = u;
sz[u] = 1;
hs[u] = -1;
for ( auto v : e[u]){
if (v == f) continue;
dfs(v,u);
// 一路递归上去
sz[u] += sz[v];
if(hs[u] == -1 || sz[v] > sz[hs[u]]){
hs[u] = v;
}
}
// 结束的dfs序列
r[u] = tot;
}
void dfs2(int u,int f ,bool keep){
for(auto v :e[u]){
if ( v!= f && v!=hs[u]){
dfs2(v,u,false);
}
}
if(hs[u] != -1){
dfs2(hs[u],u,true);
// 重儿子的集合
}
auto add =[&](int x){
x = c[x];
cnt[x]++;
if(cnt[x] > maxcnt) maxcnt = cnt[x],sumcnt=0;
// 这里有问题 应该是else if 原本是统计颜色的编号所以 改完之后 要立即加上 ,但是这个不用 判断 如果相等在家家,
//这样导值不对 不然我可以令他等于0
if (cnt[x] == maxcnt) sumcnt++;
};
auto del = [&](int x){
x = c[x];
cnt[x]--;
};
for (auto v : e[u]){
if(v!=f && v!= hs[u]){
for(int x =l[v];x<=r[v];x++)
add(id[x]);
}
}
// u本身加入
add(u);
if (maxcnt*sumcnt == sz[u]){
ans++;
}
if(!keep){
maxcnt = 0;
sumcnt = 0;
for(int x = l[u];x<=r[u];x++){
del(id[x]);
}
}
}
int main(){
scanf("%d",&n);
int a,b;
for(int i = 1;i<=n;i++){
scanf("%d%d",&a,&b);
c[i] = a;
if (b!=0){
e[b].push_back(i);
e[i].push_back(b);
}
}
dfs(1,0);
dfs2(1,0,false);
cout<<ans<<endl;
return 0;
}
总结:dsu on tree 最重要的是维护什么样子的信息,如何将题目要求翻译过来, dsu on tree 的题目套路都差不多。