『学习笔记』 dsu on tree
前置芝士
-
重链剖分(最好是熟练掌握)
-
莫队(大概了解即可,有一点相似的思想)
-
dfs序(可有可无,主要是为了加速,其实我就没写过 \(QwQ\))
主要思想
\(dsu\ on\ tree\) 又名树上启发式合并
(其实是非常暴力的一个东西。)
用途
可处理树上的一些统计类型的问题。
如:
-
求子树中颜色最多的颜色权值之和,相同数量都要加上 CF600E Lomsat gelral
-
给你一片森林,每次询问一个点与多少个点拥有共同的 \(K\) 级祖先 CF208E Blood Cousins
-
给一棵树,树上每个节点都有一个颜色,求已点 \(u\) 为根的子树中,出现次数 \(>= k\) 的颜色有多少种 CF570D Tree Requests
-
\(......\)
过程
先把处理所有已 \(u\) 为根的所有轻儿子,并统计答案。
然后删除这些轻儿子的贡献,再把重儿子加进去。
然后不用删除,直接向上传到父亲节点,也就是 \(u\) 节点即可。
怎么样,是不是听着感觉很暴力,但由于我们最后重儿子是直接继承上去的,所以复杂度可以控制在 \(log\) 内(跟树链剖分差不多)。
核心代码
dfs
用来预处理出子树大小,重儿子的信息。
与树链剖分中的代码一模一样,不贴了。
update
用于添加新的信息,或是删除不需要的信息。
inline void update(ll x, ll fa, ll type){//type = 0,添加 type = 1,删除
if(!type) add(x);
else del(x);
for(ri i = head[x]; i; i = edge[i].nxt){
ll y = edge[i].v;
if(y != fa) update(y, x, type);
}
}
solve
这个是树上启发式合并的核心部分。
inline void solve(ll x, ll fa){
for(ri i = head[x]; i; i = edge[i].nxt){
ll y = edge[i].v;
if(y == fa || y == son[x]) continue;
solve(y, x);//递归处理
update(y, x, 1);//删去轻儿子
sum = maxs = 0;//清空变量
}
sum = maxs = 0;
if(son[x]) solve(son[x], x);//处理重儿子
add(x);//加上重儿子
for(ri i = head[x]; i; i = edge[i].nxt){
ll y = edge[i].v;
if(y == fa || y == son[x]) continue;
update(y, x, 0);//加上轻儿子
}
...//更新答案
}
添加和删除操作类似于莫队,可以根据下面例题来理解。
例题
最板子的一道题了。上面核心代码就是从这道题里复制出来的
话不多说,直接看代码吧,可以看上面的注释。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#define ri register int
#define ll long long
using namespace std;
inline ll read(){
ll x = 0;
char ch = getchar();
while(ch < '0' || ch > '9') ch = getchar();
while(ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x;
}
const ll N = 1e5 + 10;
ll n;
ll a[N];
struct node{
ll v, nxt;
}edge[N << 1];
ll head[N], tot;
ll son[N], siz[N], cnt[N];
ll maxs, sum;
inline void add(ll x, ll y){
edge[++tot] = (node){y, head[x]};
head[x] = tot;
}
inline void dfs(ll x, ll fa){
siz[x] = 1;
for(ri i = head[x]; i; i = edge[i].nxt){
ll y = edge[i].v;
if(y == fa) continue;
dfs(y, x);
siz[x] += siz[y];
if(!son[x] || siz[son[x]] < siz[y])
son[x] = y;
}
}
inline void add(ll x){
cnt[a[x]]++;
ll res = cnt[a[x]];//顺便,更新出现次数最多的颜色,以及权值和。
if(res > maxs) maxs = res, sum = a[x];
else if(res == maxs) sum += a[x];
}
inline void del(ll x){
cnt[a[x]]--;
}
inline void update(ll x, ll fa, ll type){
if(!type) add(x);
else del(x);
for(ri i = head[x]; i; i = edge[i].nxt){
ll y = edge[i].v;
if(y != fa) update(y, x, type);
}
}
ll ans[N];
inline void solve(ll x, ll fa){
for(ri i = head[x]; i; i = edge[i].nxt){
ll y = edge[i].v;
if(y == fa || y == son[x]) continue;
solve(y, x);
update(y, x, 1);
sum = maxs = 0;
}
sum = maxs = 0;
if(son[x]) solve(son[x], x);
add(x);
for(ri i = head[x]; i; i = edge[i].nxt){
ll y = edge[i].v;
if(y == fa || y == son[x]) continue;
update(y, x, 0);
}
ans[x] = sum;//更新答案
}
signed main(){
n = read();
for(ri i = 1; i <= n; ++i)
a[i] = read();
for(ri i = 1; i < n; ++i){
ll u = read(), v = read();
add(u, v), add(v, u);
}
dfs(1, 0);
solve(1, 0);
for(ri i = 1; i <= n; ++i)
printf("%lld ", ans[i]);
puts("");
return 0;
}
显然有一个性质:若该串为回文串,那么出现次数为奇数的字母的个数小于等于 1。
所以我们维护一下深度的异或和即可。
emm……感觉说不清楚 \(QwQ\),还是直接看代码吧,感觉代码比较清楚些。
这道题可以用 \(bitset\) 稍微加速一下,其实没什么用,我只是为了熟练一下 \(bitset\)。
另外,要离线操作,得先把询问存下来。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#include <bitset>
using namespace std;
const int N = 5e5 + 10;
int n, m;
char c[N];
struct node{
int v, nxt;
}edge[N << 1];
int head[N], tot;
int siz[N], son[N], dep[N], ans[N];
bitset <27> sum[N];
int cnt[N][27];
struct Query{
int id, d;
};
vector <Query> q[N];
inline void add(int x, int y){
edge[++tot] = (node){y, head[x]};
head[x] = tot;
}
inline void dfs(int x, int fa){
siz[x] = 1;
dep[x] = dep[fa] + 1;
for(int i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == fa) continue;
dfs(y, x);
siz[x] += siz[y];
if(!son[x] || siz[son[x]] < siz[y])
son[x] = y;
}
}
inline void add(int x){//这边一个函数就够了,因为是异或操作
sum[dep[x]][c[x] - 'a'] = sum[dep[x]][c[x] - 'a'] ^ 1;
}
inline void update(int x, int fa){
add(x);
for(int i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == fa) continue;
update(y, x);
}
}
inline void solve(int x, int fa){
for(int i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == fa || y == son[x]) continue;
solve(y, x);
update(y, x);
}
if(son[x]) solve(son[x], x);
add(x);
for(int i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == fa || y == son[x]) continue;
update(y, x);
}
for(auto y : q[x]) ans[y.id] = (sum[y.d].count() <= 1);//更新答案
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 2, x; i <= n; i++){
scanf("%d", &x);
add(x, i), add(i, x);
}
scanf("%s", c + 1);
for(int i = 1, a, b; i <= m; i++){
scanf("%d%d", &a, &b);
q[a].push_back((Query){i, b});
}
dfs(1, 0);
solve(1, 0);
for(int i = 1; i <= m; i++)
puts(ans[i] ? "Yes" : "No");
return 0;
}
有没有惊奇的发现代码没什么区别呢。
那我们发现这道题和例一似乎没什么区别……
先开个颜色桶,然后再对颜色出现次数做个类似于前缀和的东西,就是每次更新颜色出现次数的时候,令记录出现次数的桶 \(++\)。
直接看代码吧。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
int n, m, k;
int c[N], siz[N], son[N];
struct Query{
int id, k;
};
vector <Query> g[N];
struct node{
int v, nxt;
}edge[N << 1];
int head[N], tot;
int cnt[N], ans[N];
int col[N], sum[N];//col:颜色桶 sum:出现次数桶
inline void add(int x, int y){
edge[++tot] = (node){y, head[x]};
head[x] = tot;
}
inline void dfs(int x, int fa){
siz[x] = 1;
for(int i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == fa) continue;
dfs(y, x);
siz[x] += siz[y];
if(!son[x] || siz[son[x]] < siz[y])
son[x] = y;
}
}
inline void add(int x){
col[c[x]]++, sum[col[c[x]]]++;//经过 k 次时都会 + 1,所以下面直接 = sum[k] 即可
}
inline void del(int x){
sum[col[c[x]]]--, col[c[x]]--;
}
inline void update(int x, int fa, int type){
if(!type) add(x);
else del(x);
for(int i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == fa) continue;
update(y, x, type);
}
}
inline void solve(int x, int fa){
for(int i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == fa || y == son[x]) continue;
solve(y, x);
update(y, x, 1);
}
if(son[x]) solve(son[x], x);
add(x);
for(int i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == fa || y == son[x]) continue;
update(y, x, 0);
}
for(auto y : g[x]) ans[y.id] = sum[y.k];//统计答案,原因在上面注释中解释了
}
int main(){
scanf("%d%d", &n, &m);
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);
add(u, v), add(v, u);
}
for(int i = 1; i <= m; i++){//离线操作
int u, k;
scanf("%d%d", &u, &k);
g[u].push_back(Query{i, k});
}
dfs(1, 0);
solve(1, 0);
for(int i = 1; i <= m; i++)
printf("%d\n", ans[i]);
return 0;
}
神奇的发现代码还是没什么区别。
这道题就有点意思了。
我们先把题目转化一下,变成查找 \(u\) 结点,的第 \(k\) 级儿子有多少个。
所以我们要倍增查一下祖先,然后 \(add\) 函数 \(++\)。
统计答案的时候是 \(cnt[dep[x] + k]\),要把自己本身的深度也加上。
嗯,大概就这样。
看代码吧。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#define ri register int
using namespace std;
inline int read(){
int x = 0;
char ch = getchar();
while(ch < '0' || ch > '9') ch = getchar();
while(ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x;
}
const int N = 1e5 + 10;
int n, m;
struct node{
int v, nxt;
}edge[N << 1];
struct Query{
int id, k;
};
vector <Query> q[N];
int head[N], tot;
int fa[N][18], cnt[N];
int siz[N], dep[N], son[N], ans[N];
inline void add(int x, int y){
edge[++tot] = (node){y, head[x]};
head[x] = tot;
}
inline void dfs(int x, int f){
siz[x] = 1, dep[x] = dep[f] + 1;
for(ri i = 1; i <= 18; i++)//预处理倍增数组
if(fa[fa[x][i - 1]][i - 1])
fa[x][i] = fa[fa[x][i - 1]][i - 1];
for(ri i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == f) continue;
dfs(y, x);
siz[x] += siz[y];
if(!son[x] || siz[son[x]] < siz[y])
son[x] = y;
}
}
inline int find_fa(int x, int k){//倍增查找k级祖先
for(ri i = 18; i >= 0; i--)
if(k >= (1 << i))
k -= (1 << i), x = fa[x][i];
return x;
}
inline void add(int x){
cnt[dep[x]]++;
}
inline void del(int x){
cnt[dep[x]] = 0;
}
inline void update(int x, int type){
if(!type) add(x);
else del(x);
for(ri i = head[x]; i; i = edge[i].nxt)
update(edge[i].v, type);
}
//这个solve有一点改动,主要是因为我之前那种写法挂了,调了好久都没调出来QWQ
inline void solve(int x, int flag){//flag标记重儿子还是轻儿子,= 1 是重儿子
for(ri i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == son[x]) continue;
solve(y, 0);//处理轻儿子
}
if(son[x]) solve(son[x], 1);//重儿子
for(int i = head[x]; i; i = edge[i].nxt){
int y = edge[i].v;
if(y == son[x]) continue;
update(y, 0);//加入轻儿子
}
add(x);//加入根
for(auto y : q[x]) ans[y.id] = cnt[dep[x] + y.k];//统计答案,这里是节点 x 的向下 k 级祖先,所以深度为 dep[x] + y.k
if(!flag) update(x, 1);//如果是轻儿子,就删除信息
}
int main(){
n = read();
for(ri i = 1; i <= n; i++){
int x = read();
fa[i][0] = x;
add(x, i);
}
for(ri i = 1; i <= n; i++)
if(!fa[i][0])//注意这是个森林
dfs(i, 0);
m = read();
for(ri i = 1; i <= m; i++){//离线
int u = read(), k = read();
int p = find_fa(u, k);
if(p) q[p].push_back((Query){i, k});//记录询问
}
for(ri i = 1; i <= n; i++)
if(!fa[i][0]) solve(i, 0);//森林
for(ri i = 1; i <= m; i++)
printf("%d ", max(ans[i] - 1, 0));//要减去查找的那个点
puts("");
return 0;
}
总结
暴力!暴力!暴力!
好吧,没什么好说的,全都是套路。
更新节点,统计答案不同的题不同操作,其他的都一样。