[dsu on tree]树上启发式合并总结(算法思想及模板附例题练习)
前言
最近不是在⛏李超树嘛,然后就去玩了下线段树,顺道碰见了线段树合并,想起了线段树分治,发现自己连点分治都不会,于是多管齐下,先把树上启发式合并⛏明白再说
树上启发式合并
引入
说到启发式合并,我们最先想到的是什么??
————就是并查集
普通版
void makeSet( int n ) {
for( int i = 1;i <= n;i ++ ) f[i] = i;
}
int find( int x ) {
return ( f[x] == x ) ? x : f[x] = find( f[x] );
}
void unionSet( int u, int v ) {
int fu = f[u], fv = f[v];
f[fv] = fu;
}
并查集按秩合并
对于两个大小不一样的集合,将大小 小的并到大的
这个集合的大小可以认为是集合的高度(在正常情况下)
而我们将集合高度小的并到高度大的有助于我们找到父亲
让高度小的树成为高度较大的树的子树,这个优化可以称为启发式合并算法
void makeSet( int n ) {
for( int i = 1;i <= n;i ++ ) f[i] = i, siz[i] = 1;
}
int find( int x ) {
return ( f[x] == x ) ? x : f[x] = find( f[x] );
}
void unionSet( int u, int v ) {
int fu = f[u], fv = f[v];
if( fu == fv ) return;
if( siz[fu] < siz[fv] ) swap( fu, fv );
f[fv] = fu, siz[fu] += siz[fv];
}
而我们现在即将要见到的神秘加冰也是启发式合并的一类——树上启发式合并!!
树上启发式合并又叫\(dsu\ on\ tree\)
算法思想
首先发生地点是在树上
用一道例题将思想由抽象化为具象讲解
其他AC算法直接踹飞,不考虑哈
人最原始的欲望就是性和杀戮
最暴力的算法就是直接硬刚,对每一个点都暴力搞子树里面所有的点——\(O(n^2)\)
然后就可以成功\(T\)飞
我们考虑暴力算法的时间复杂度为什么会这么高,究竟是卡在哪里了??
树的遍历肯定是建立在\(dfs\)上的 你难不成搞bfs 看图👇
暴力的想法很简单——碰到一个点,就暴算此点的答案
那么这个实现应该是从下往上计算的
当我们遍历到\(u\)的时候,继续去弄\(son1\)
假设已经搞定了\(son1\)子树所有点,接着就马不停蹄地去搞\(son2\),同理到\(son3\)
最后再来一遍\(u\)
发现某些节点啊,总是加了删,删了加 工具人石锤
因为\(u\)各个儿子之间是彼此独立的,相互之间的答案是不造成干扰的
而这些加删操作时间复杂度就是该子树大小
这不上天谁上天??非常恐怖兄嘚
那怎么办呢?
暴力这个概念的存在就是为了和优化形成对应
在这个基础上,树上启发式合并的思想就成功诞生了!!
算法思想
- 首先\(O(n)\)遍历一遍树,求出每个点的重儿子——没错就是树链剖分的那个意思
- 然后就可以开始搞答案了
2-1 先把\(u\)点所有子树的答案都计算完了,再计算\(u\)——自下而上
2-2 轻儿子的所有贡献都要删掉——各自独立
2-3 重儿子的贡献不删掉
因此,2-2,2-3决定了算法计算有一定的顺序先轻儿子再重儿子
看似只是换了个计算顺序,只是有一个儿子没有暴力操作,其它的跟暴力完全一样啊
但是这样就将时间复杂度变成了\(O(nlogn)\)
时间复杂度
像树链剖分一样定义轻边重边轻链重链轻儿子重儿子
红边表示重边,黑边表示轻边,黄点表示该点身份是父亲的重儿子
因为重儿子的\(size\)一定\(\ge\)轻儿子的\(size\)
所以父亲的\(size\)一定\(\ge\)轻儿子\(size\)的两倍
也就是说,每往上一层,新的父亲的\(size\)至少是前一层的两倍
等价于,可以说一个点到根节点的路径,不会有超过\(log\ n\)条轻边
又因为树上启发式合并的操作,重儿子的子树是不会有倒退操作的,只有轻儿子的子树才会
所以,一个点\(x\)被遍历操作的次数取决于他到根节点路径上的轻边数+1
ps: \(+1\)是因为他本身要被遍历到
按最极限的最坏(每个点被遍历的次数尽量多) 情况考虑
一个节点的被遍历次数也大约只为\(=logn+1\)
总时间复杂度则为\(O(n(logn+1))=O(nlogn)\)
模板
还是意思意思,给一个并没有什么卵用的模板吧
void dfs1( int u, int fa ) { //遍历整个树,找每个点的重儿子
siz[u] = 1;
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa ) continue;
dfs1( v, u );
siz[u] += siz[v];
if( ! son[u] || siz[v] > siz[son[u]] )
son[u] = v;
}
}
void modify( int u, int fa, bool flag ) {//flag=0回退 flag=1添加
if( flag ) {
//搞一搞
}
else {
//搞一搞
}
for( int i = 0;i < G[u].size();i ++ )
if( G[u][i] != fa ) modify( G[u][i], u, flag );
}
void dfs2( int u, int fa ) {
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa || v == son[u] ) continue;
dfs2( v, u );//先算轻儿子的答案
modify( v, u, 0 );//计算完轻儿子的答案后 要把儿子的痕迹擦干净 为下一个儿子准备
//...可能还有其他的操作
}
if( son[u] ) dfs2( son[u], u );//重儿子的贡献仍然保留 不回退
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa || v == son[u] ) continue;
else modify( v, u, 1 );//开始重新添加每个轻儿子的贡献 为后面计算自己准备
}
//...一堆操作
}
有等价写法,下面练习的代码基本上是用的这一种
void dfs1( int u, int fa ) {
siz[u] = 1;
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa ) continue;
dfs1( v, u ), siz[u] += siz[v];
if( siz[v] > siz[son[u]] || ! son[u] )
son[u] = v;
}
}
void modify( int u, int fa, int k ) {
//操作一下
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa || vis[v] ) continue;
modify( v, u, k );
}
}
void dfs2( int u, int fa, int type ) {
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa || v == son[u] ) continue;
dfs2( v, u, 0 );//此时先不着急
}
if( son[u] ) dfs2( son[u], u, 1 ), vis[son[u]] = 1;
modify( u, fa, 1 );//把所有轻儿子的贡献统计上
//操作
if( son[u] ) vis[son[u]] = 0;
if( ! type ) modify( u, fa, -1 );//在这里直接遍历整棵树开始回退 因此需要特别对重儿子打个标记
}
练习
例题:CF600E Lomsat gelral
solution
就是前面用来举例子的题,比较板,可能读题会有所误会
\(num[i]\):颜色\(i\)出现的次数
\(cnt\):颜色出现最多的次数
\(ans\):颜色出现次数最多的所有颜色的和
然后直接搞就可以了,具体可见代码
code
#include <cstdio>
#include <vector>
using namespace std;
#define maxn 100005
#define int long long
vector < int > G[maxn];
int n, cnt, ans;
int c[maxn], son[maxn], siz[maxn], num[maxn], result[maxn];
void dfs1( int u, int fa ) {
siz[u] = 1;
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa ) continue;
dfs1( v, u );
siz[u] += siz[v];
if( ! son[u] || siz[v] > siz[son[u]] )
son[u] = v;
}
}
void modify( int u, int fa, bool flag ) {//0回退 1添加
if( flag ) {
num[c[u]] ++;
if( num[c[u]] > cnt )
cnt = num[c[u]], ans = c[u];
else if( num[c[u]] == cnt ) ans += c[u];
}
else
num[c[u]] --;
for( int i = 0;i < G[u].size();i ++ )
if( G[u][i] != fa ) modify( G[u][i], u, flag );
}
void dfs2( int u, int fa ) {
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa || v == son[u] ) continue;
dfs2( v, u );
modify( v, u, 0 );//计算完轻儿子的答案后 要把儿子的痕迹擦干净
cnt = ans = 0;
}
if( son[u] ) dfs2( son[u], u );//重儿子的贡献仍然保留
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa || v == son[u] ) continue;
else modify( v, u, 1 );
}
num[c[u]] ++;
if( num[c[u]] > cnt ) ans = c[u], cnt = num[c[u]];
else if( num[c[u]] == cnt ) ans += c[u];
result[u] = ans;
}
signed main() {
scanf( "%lld", &n );
for( int i = 1;i <= n;i ++ )
scanf( "%lld", &c[i] );
for( int i = 1, u, v;i < n;i ++ ) {
scanf( "%lld %lld", &u, &v );
G[u].push_back( v );
G[v].push_back( u );
}
dfs1( 1, 0 );
dfs2( 1, 0 );
for( int i = 1;i <= n;i ++ )
printf( "%lld ", result[i] );
return 0;
}
CF208E Blood Cousins
solution
可以考虑加一个假根\(0\),将森林变成一棵树 这个操作很常见
然后考虑转换一下题意=>\(u\)的\(k\)级祖先的儿子有多少个,变成求同深度的\(u\)的兄弟伙个数
code
#include <cstdio>
#include <vector>
using namespace std;
#define ll long long
#define N 100005
vector< int > G[N];
vector< pair < int, int > > query[N];
int n, m;
bool vis[N];
int son[N], dep[N], Size[N];
ll cnt[N], ans[N];
int f[N][20];
void dfs1( int u, int fa ) {
Size[u] = 1;
dep[u] = dep[fa] + 1;
f[u][0] = fa;
for( int i = 1;i < 20;i ++ )
f[u][i] = f[f[u][i - 1]][i - 1];
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa ) continue;
dfs1( v, u ), Size[u] += Size[v];
if( Size[v] > Size[son[u]] || ! son[u] )
son[u] = v;
}
}
int kthF( int u, int k ) {
for( int i = 0;i < 20;i ++ )
if( ( 1 << i ) & k ) u = f[u][i];
return u;
}
void modify( int u, int fa, int k ) {
cnt[dep[u]] += k;
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa || vis[v] ) continue;
modify( v, u, k );
}
}
void dfs2( int u, int fa, int type ) {
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa || v == son[u] ) continue;
dfs2( v, u, 0 );
}
if( son[u] ) dfs2( son[u], u, 1 ), vis[son[u]] = 1;
modify( u, fa, 1 );
for( int i = 0;i < query[u].size();i ++ )//-1是减去自己
ans[query[u][i].second] = cnt[query[u][i].first + dep[u]] - 1;
if( son[u] ) vis[son[u]] = 0;
if( ! type ) modify( u, fa, -1 );
}
signed main() {
scanf( "%d", &n );
for( int i = 1, x;i <= n;i ++ ) {
scanf( "%d", &x );
G[i].push_back( x );
G[x].push_back( i );
}
dfs1( 0, 0 );
scanf( "%d", &m );
for( int i = 1, u, k;i <= m;i ++ ) {
scanf( "%d %d", &u, &k );
int Fa = kthF( u, k );//找到自己的k级父亲 用lca跑
if( ! Fa ) continue;
query[Fa].push_back( make_pair( k, i ) );
}
dfs2( 0, 0, 0 );
for( int i = 1;i <= m;i ++ ) printf( "%lld ", ans[i] );
return 0;
}
CF570D Tree Requests
solution
题目已经有所放水,因为可以把字符打乱排序
所以肯定是\(\le1\)个字符出现了奇数次,其它字符必须出现偶数次
对出现了奇数次的字符个数进行维护即可
code
#include <cstdio>
#include <vector>
using namespace std;
#define N 500005
vector < int > G[N];
vector < pair < int, int > > query[N];
int n, m;
int t[N], son[N], dep[N], num[N], tot[N], ans[N], Size[N];
bool vis[N];
int cnt[N][30];
void dfs1( int u, int fa ) {
Size[u] = 1, dep[u] = dep[fa] + 1;
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa ) continue;
dfs1( v, u );
Size[u] += Size[v];
if( ! son[u] || Size[v] > Size[son[u]] )
son[u] = v;
}
}
void modify( int u, int fa, int k ) {
cnt[dep[u]][t[u]] += k;
num[dep[u]] += k;
if( cnt[dep[u]][t[u]] & 1 ) tot[dep[u]] ++;
else tot[dep[u]] --;
for( int i = 0;i < G[u].size();i ++ )
if( G[u][i] != fa && ! vis[G[u][i]] )
modify( G[u][i], u, k );
}
void dfs2( int u, int fa, bool flag ) {
for( int i = 0;i < G[u].size();i ++ )
if( G[u][i] != fa && G[u][i] != son[u] )
dfs2( G[u][i], u, 0 );
if( son[u] ) dfs2( son[u], u, 1 ), vis[son[u]] = 1;
modify( u, fa, 1 );
for( int i = 0;i < query[u].size();i ++ )
ans[query[u][i].second] = ( num[query[u][i].first] == 0 || tot[query[u][i].first] <= 1 );
if( son[u] ) vis[son[u]] = 0;
if( ! flag ) modify( u, fa, -1 );
}
char s[N];
int main() {
scanf( "%d %d", &n, &m );
for( int i = 2, x;i <= n;i ++ ) {
scanf( "%d", &x );
G[x].push_back( i );
G[i].push_back( x );
}
scanf( "%s", s );
for( int i = 0;i < n;i ++ )
t[i + 1] = s[i] - 'a';
dfs1( 1, 0 );
for( int i = 1, x, y;i <= m;i ++ ) {
scanf( "%d %d", &x, &y );
query[x].push_back( make_pair( y, i ) );
}
dfs2( 1, 0, 1 );
for( int i = 1;i <= m;i ++ )
if( ans[i] ) printf( "Yes\n" );
else printf( "No\n" );
return 0;
}
CF1009F Dominant Indices
solution
与例题的维护操作差不多,这里用的是\(dep\)深度差求\(k\)
code
#include <cstdio>
#include <vector>
using namespace std;
#define N 1000005
vector < int > G[N];
int n, Max;
int son[N], dep[N], Size[N];
int ans[N], cnt[N];
bool vis[N];
void dfs1( int u, int fa ) {
Size[u] = 1, dep[u] = dep[fa] + 1;
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
if( v == fa ) continue;
dfs1( v, u );
Size[u] += Size[v];
if( ! son[u] || Size[v] > Size[son[u]] )
son[u] = v;
}
}
void modify( int u, int fa, int k ) {
cnt[dep[u]] += k;
if( cnt[dep[u]] > cnt[Max] || ( cnt[dep[u]] == cnt[Max] && dep[u] < Max ) )
Max = dep[u];
for( int i = 0;i < G[u].size();i ++ )
if( G[u][i] != fa && ! vis[G[u][i]] )
modify( G[u][i], u, k );
}
void dfs2( int u, int fa, bool flag ) {
for( int i = 0;i < G[u].size();i ++ )
if( G[u][i] != fa && G[u][i] != son[u] )
dfs2( G[u][i], u, 0 );
if( son[u] ) dfs2( son[u], u, 1 ), vis[son[u]] = 1;
modify( u, fa, 1 );
ans[u] = Max - dep[u];
if( son[u] ) vis[son[u]] = 0;
if( ! flag ) modify( u, fa, -1 );
}
int main() {
scanf( "%d", &n );
for( int i = 1, u, v;i < n;i ++ ) {
scanf( "%d %d", &u, &v );
G[u].push_back( v );
G[v].push_back( u );
}
dfs1( 1, 0 );
dfs2( 1, 0, 1 );
for( int i = 1;i <= n;i ++ )
printf( "%d\n", ans[i] );
return 0;
}