『学习笔记』 dsu on tree

前置芝士

  • 重链剖分(最好是熟练掌握)

  • 莫队(大概了解即可,有一点相似的思想)

  • dfs序(可有可无,主要是为了加速,其实我就没写过 \(QwQ\)

主要思想

\(dsu\ on\ tree\) 又名树上启发式合并

(其实是非常暴力的一个东西。)

用途

可处理树上的一些统计类型的问题。

如:

  1. 求子树中颜色最多的颜色权值之和,相同数量都要加上 CF600E Lomsat gelral

  2. 给你一片森林,每次询问一个点与多少个点拥有共同的 \(K\) 级祖先 CF208E Blood Cousins

  3. 给一棵树,树上每个节点都有一个颜色,求已点 \(u\) 为根的子树中,出现次数 \(>= k\) 的颜色有多少种 CF570D Tree Requests

  4. \(......\)

过程

先把处理所有已 \(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);//加上轻儿子
    }
    ...//更新答案
}

添加和删除操作类似于莫队,可以根据下面例题来理解。

例题

  1. CF600E Lomsat gelral

最板子的一道题了。上面核心代码就是从这道题里复制出来的

话不多说,直接看代码吧,可以看上面的注释。

#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. CF570D Tree Requests

显然有一个性质:若该串为回文串,那么出现次数为奇数的字母的个数小于等于 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;
}

有没有惊奇的发现代码没什么区别呢。

  1. CF375D Tree and Queries

那我们发现这道题和例一似乎没什么区别……

先开个颜色桶,然后再对颜色出现次数做个类似于前缀和的东西,就是每次更新颜色出现次数的时候,令记录出现次数的桶 \(++\)

直接看代码吧。

#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;
}

神奇的发现代码还是没什么区别。

  1. CF208E Blood Cousins

这道题就有点意思了。

我们先把题目转化一下,变成查找 \(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;
}

总结

暴力!暴力!暴力!

好吧,没什么好说的,全都是套路。

更新节点,统计答案不同的题不同操作,其他的都一样。

\[\_EOF\_ \]

posted @ 2021-10-02 22:35  xixike  阅读(84)  评论(2编辑  收藏  举报