前言
树上启发式合并是一种暴力算法的优化,主要优化的是暴力算法的“合并”过程,其思路主要是:如果节点x的信息能通过它的子树合并得来,那么我们可以选择一棵子树的信息(这样这颗子树就不用再遍历一遍了),把其他子树的信息暴力合并过来,就变成了节点x的信息。
我们一般选择重儿子作为预先的信息。这样将获得O(nlogn)的复杂度,这是因为会暴力统计轻儿子的信息,而一个节点到根节点最多有log(n)条轻边,所以每一个节点最多会被暴力统计log次,总共就是O(nlogn)。
所以说,树上启发式合并(DSU on Tree)的关键在于继承重儿子的信息,如果在统计过程中,又暴力统计的重儿子,那么复杂度就错了。
例题
具体过程直接截孙铭远的课件了:
代码如下:
#include<iostream>
#include<vector>
using namespace std;
vector<vector<int>> a;
int h[100005],size[100005],son[100005];
int n;
int dfs1(int u,int fa) {
size[u]=1;
for(auto&v:a[u]) {
if(v==fa) continue;
size[u]+=dfs1(v,u);
if(size[son[u]]<size[v]) son[u]=v;
}
return size[u];
}
int t[100005],queue[100005],top;
int maxx[100005];
long long ans[100005];
void calc(int u,int k) {
t[h[u]]++;
if(t[h[u]]>maxx[k]) ans[k]=h[u],maxx[k]=t[h[u]];
else if(t[h[u]]==maxx[k]) ans[k]+=h[u];
}
void dfs3(int u,int fa,int k) {//统计
calc(u,k);
for(auto&v:a[u])
if(v^fa)
dfs3(v,u,k);
}
void dfs4(int u,int fa) {//清空
t[h[u]]--;
for(auto&v:a[u])
if(v^fa)
dfs4(v,u);
}
void dfs5(int u,int fa) {//求解
for(auto&v:a[u])
if(v^fa&&v^son[u])
dfs5(v,u),dfs4(v,u);
if(son[u]) dfs5(son[u],u);
ans[u]+=ans[son[u]];
maxx[u]=maxx[son[u]];
for(auto&v:a[u])
if(v^fa&&v^son[u])
dfs3(v,u,u);
calc(u,u);
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>h[i];
for(int i=0;i<=n;i++) a.push_back({});
for(int i=1;i<n;i++) {
int u,v;
cin>>u>>v;
a[u].push_back(v);
a[v].push_back(u);
}
dfs1(1,0);
// for(int i=1;i<=n;i++)
// cout<<size[i]<<' '<<son[i]<<endl;
dfs5(1,0);
for(int i=1;i<=n;i++)
cout<<ans[i]<<' ';
return 0;
}
- 不要忘了继承重儿子的信息
- 还是说,保证重儿子只会走一遍,否则复杂度就是错的
后记
于是皆大欢喜。