树上启发式合并
前言: 今天又遇到启发式合并了,还没有写出来,还是整理一下。
什么时候用启发式合并?
当需要统计并合并子树的信息,然而一次合并的复杂度为 O ( n ) O(n) O(n),整体复杂度为 O ( n 2 ) O(n^2) O(n2)时,使用启发式合并可以将合并的复杂度降为 O ( n l o g n ) O(nlogn) O(nlogn)。
启发式合并的思想
举一个例子,现在有一棵树,每个节点涂有一个颜色。需要统计每颗子树上哪种颜色最多。
暴力合并:
//实际上有更简洁的代码,这么写是为了和后面的启发式合并的代码做比较,有更直观的感受。
nt n;//size of tree
vector<int> tree[maxn];
int color[maxn];//color on node
int ans[maxn];// ans for each node
int cnt[maxn];//used for cnt color on subtree
int dfs(int rt, int fa, int *cnt)
{
for(auto v : tree[rt])
{
if(v == fa)continue;
ans[v] = dfs(v, rt, cnt);
memset(cnt, 0, sizeof(cnt));
}
for(auto v : tree[rt])
{
if(v == fa)continue;
dfs(v, rt, cnt);
}
cnt[color[rt]]++;
int mx = -1, pos = 0;
for(int i = 1; i <= colorMax; ++i)
if(mx < cnt[i])mx = cnt[i], pos = i;
return pos;
}
上面的算法流程可以总结为:
- 统计子节点的信息并记录子节点答案
- 重新统计子节点的信息,合并在一块
- 加入父亲节点信息,计算答案
每颗子树被统计至少一遍,复杂度
O
(
n
2
)
O(n^2)
O(n2)。
思考,可不可以在统计子节点信息,直接把子节点信息写入父亲节点相关数据结构,而不需要合并。
答案是,只能选一个子节点在统计时直接写入父亲节点数据结构。如果选重儿子这么做,就是树上启发式合并。
算法流程
- 统计轻儿子的信息并记录轻儿子的答案
- 统计重儿子的信息记录答案,同时保留重儿子的信息
- 将轻儿子的信息加入记录
- 加入父亲节点信息,计算答案。
每个节点点被统计的次数为到根节点路径上轻链的数量 + 1,由于一条路径上轻链的个数不会超过 l o g n logn logn条,所以算法的复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
大概代码:
int n;//size of tree
vector<int> tree[maxn];
int color[maxn];//color on node
int ans[maxn];// ans for each node
int cnt[maxn];//used for cnt color on subtree
int son[maxn];// heavy son
int dfs(int rt, int fa, int *cnt)
{
if(!son[rt])//a leaf
{
cnt[color[rt]]++;
return;
}
//cal light son
for(auto v : tree[rt])
{
if(v == son[rt] || v == fa)continue;
ans[v] = dfs(v, rt, cnt);
memset(cnt, 0, sizeof(cnt));
}
//cal heavy son and remain the data
dfs(son[rt], rt, cnt);
//add light son data to get rt ans
for(auto v : tree[rt])
if(v == fa || v == son[rt])continue;
else dfs(v, rt, cnt);
cnt[color[rt]]++;
int mx = -1, pos = 0;
for(int i = 1; i <= colorMax; ++i)
if(mx < cnt[i])mx = cnt[i], pos = i;
return pos;
}