dsu on tree 简单总结

什么是dsu on tree:

对于一棵树,如果我们需要统计每一个点的子树的信息,比如求子树中哪个点权出现的次数最多(最大值这种就是树剖裸题了搞什么$dsu$ $on$ $tree$?)这样子,如果我们对于每一个子树都进行遍历的话,时间复杂度将达到$O(n^2)$,当$n \leq 1e5$的时候,这个时间复杂度无法接受。那么我们应该怎么办呢?这个时候就需要使用$dsu$ $on$ $tree$。

我们知道,树链剖分可以把一棵树分成$O(log_2 n)$条重链,剩下的都是轻边。现在证明一下从根到某结点经过的轻边的数量:因为每经过一条轻边,结点数是一半,所以$size \leq \frac{n}{2^{edge}}$,又$n > 2^{edge}$,所以$edge<log_2 n$。即只会经过$O(log_2 n)$条轻边。

我们先从上到下遍历结点,然后对于某个节点,我们访问所有的轻儿子,然后求得了这些轻儿子的子树信息,接下来我们考虑重儿子。访问重儿子,然后保留重儿子的信息,对于轻儿子就暴力处理。我们分析它的时间复杂度:我们假设这棵子树的大小是$n$。则我们暴力统计所有轻儿子的时间复杂度是$O(log_2 n)$,保留重儿子信息,即重儿子信息$O(1)$上推,则我们就可以$O(log_2 n)$求出这棵子树的信息。

当然,树不能有修改,不然重儿子的信息就会失效。

并且,所有询问都针对子树。

其思想是启发式合并,因为我还没有学,所以先这样,学了再回来总结。

dsu on tree实现:

算法流程:

一、轻重链剖分

然后这个过程不好理解,我给出路径:

进入轻儿子直到叶节点,处理这个叶节点的信息,然后更新这个轻儿子答案,并删除轻儿子的信息,然后回溯到它的父亲,然后进入重儿子,假设这个重儿子是叶节点,我们处理重儿子的信息,保留信息并更新答案,然后回溯到它的父亲,它的父亲将会暴力统计这个轻儿子的信息,然后更新自己(因为重儿子信息没有删除),然后删除轻儿子的信息,这个节点处理完,回溯到上一层,这样子我们就保留了重儿子的信息以处理这个子树根结点的父亲。显然,因为本身我们进入的是一个轻儿子,所以在处理完它的父节点时,这个轻儿子的信息会被删除(这个建议画图理解,非常绕)。

例题:

1、codeforces600E Lomsat gelral

题意:

给出一棵树,求出每个节点的子树中出现次数最多的颜色的编号和。

题解:

直接$dsu$ $on$ $tree$,用桶记录颜色次数即可。

AC代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 5;
vector<int> G[MAXN];
int col[MAXN], son[MAXN], sz[MAXN];
bool vis[MAXN];
ll tot[MAXN], ans[MAXN];
ll maxn, c;
void dfs1(int u, int fa)
{
    sz[u] = 1;
    for (auto i : G[u])
        if (i != fa)
        {
            dfs1(i, u);
            sz[u] += sz[i];
            if (sz[i] > sz[son[u]])
                son[u] = i;
        }
}
void update(int u, int fa, int p)
{
    tot[col[u]] += p;
    if (p > 0 && maxn == tot[col[u]])
        c += col[u];
    else if (p > 0 && maxn < tot[col[u]])
        c = col[u], maxn = tot[col[u]];
    for (auto i : G[u])
        if (i != fa && !vis[i])
            update(i, u, p);
}
void dfs2(int u, int fa, int op)
{
    for (auto i : G[u])
        if (i != son[u] && i != fa)
            dfs2(i, u, 0); 
    if (son[u])
        dfs2(son[u], u, 1), vis[son[u]] = 1;
    update(u, fa, 1), ans[u] = c;
    if (son[u])
        vis[son[u]] = 0;
    if (!op)
        update(u, fa, -1), maxn = c = 0;
}
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &col[i]);
    int a, b;
    for (int i = 1; i < n; ++i)
    {
        scanf("%d%d", &a, &b);
        G[a].push_back(b);
        G[b].push_back(a);
    }
    dfs1(1, 0);       //tot: n
    dfs2(1, 0, 0); //tot: n+2nlogn
    for (int i = 1; i <= n; ++i)
        printf("%lld%c", ans[i], i == n ? '\n' : ' ');
    return 0;
}
View Code

注:为什么$update$之后$vis[son[u]]$要置零,这是因为,只要$op$是$0$,这个对于其父亲来说轻儿子,所以实际上执行到这里的时候,它的父亲的重儿子的信息处理好了,所以就要把这个轻儿子的所有信息全部删去,这个很绕。

2、codeforces570D Tree Requests

题意:

给出一棵树,树上每个节点上有一个小写字符,给出一些询问,询问包括节点和深度,求出以这个节点为根,固定深度的节点上能否组成一个回文串。

题解:

看满不满足使用条件:无修改,查询子树。满足条件,然后我们看维护什么,维护一个回文串,这个其实不好做,显然我们不能把这些深度的字符串都保存下来,因为这将达到$O(n^2)$的空间,所以我们只能用更加省空间的方案。一个字符串构成回文串,里面的同种字符的数量至多出现一个奇数,所以我们实际关注的是,这些字符串模$2$的和是不是小于$2$,因为模$2$就是二进制,所以我们直接用一个整数维护这个字符串,用异或增减字符即可。

为什么我们这里可以处理相对某个结点深度大于$1$的结点呢?这是因为处理完重儿子之后到删除轻儿子信息之前,这棵子树的任何一个结点除了它自己的信息都是知道的,因为重儿子信息被保存,轻儿子子树的所有结点信息被处理出来,所以就可以把这个子树为根的询问全部回答了。

AC代码:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5e5 + 5;
vector<int> G[MAXN];
struct Q
{
    int pos, dep;
    Q(int _pos, int _dep) : pos(_pos), dep(_dep) {}
};
vector<Q> q[MAXN];
bool ans[MAXN];
int dep[MAXN], val[MAXN], sz[MAXN];
int tot[MAXN], son[MAXN];
bool vis[MAXN];
void dfs1(int u, int fa)
{
    dep[u] = dep[fa] + 1;
    sz[u] = 1;
    for (auto i : G[u])
        if (i != fa)
        {
            dfs1(i, u);
            sz[u] += sz[i];
            if (sz[i] > sz[son[u]])
                son[u] = i;
        }
}
bool get1(int val)
{
    int rt = 0;
    while (val)
        rt += val % 2, val >>= 1;
    return rt < 2;
}
void update(int u, int fa)
{
    tot[dep[u]] ^= val[u];
    for (auto i : G[u])
        if (i != fa && !vis[i])
            update(i, u);
}
void del(int u, int fa)
{
    tot[dep[u]] = 0;
    for (auto i : G[u])
        if (i != fa && !vis[i])
            del(i, u);
}
void dfs2(int u, int fa, int op)
{
    for (auto i : G[u])
        if (i != fa && i != son[u])
            dfs2(i, u, 0);
    if (son[u])
        dfs2(son[u], u, 1), vis[son[u]] = 1;
    update(u, fa);
    for (int i = 0; i < q[u].size(); ++i)
        ans[q[u][i].pos] = get1(tot[q[u][i].dep]);
    if (son[u])
        vis[son[u]] = 0;
    if (!op)
        del(u, fa);
}
char str[MAXN];
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    int a;
    for (int i = 1; i < n; ++i)
    {
        scanf("%d", &a);
        G[i + 1].push_back(a);
        G[a].push_back(i + 1);
    }
    scanf("%s", str + 1);
    for (int i = 1; i <= n; ++i)
        val[i] = 1 << (str[i] - 'a');
    int x, y;
    for (int i = 1; i <= m; ++i)
    {
        scanf("%d%d", &x, &y);
        q[x].push_back(Q(i, y));
    }
    dfs1(1, 0);
    dfs2(1, 0, 0);
    for (int i = 1; i <= m; ++i)
        printf(ans[i] ? "Yes\n" : "No\n");
    return 0;
}
View Code

3、codeforces246E Blood Cousins Return

题意:

给出一棵族谱树,每个人都有一个名字,给出一些询问,求某个人的第$k$级儿子中有多少个不同的名字。

题解:

静态的子树信息询问,显然就是$dsu$ $on$ $tree$。用一个$set$维护每个深度的人名信息即可。注意:第$k$级儿子的深度可能超过树的最大深度,这个时候输出$0$,但是必须开够$set$否则会$RE$或者$WA$。

AC代码:(代码中的名字进行了离散化)

#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
set<int> s[N << 1];
vector<int> G[N];
pair<int, int> p[N];
vector<pair<int, int>> q[N];
int ans[N];

string a[N], b[N];
int id[N];

int dep[N], son[N], sz[N];
void dfs1(int u, int fa)
{
    sz[u] = 1;
    dep[u] = dep[fa] + 1;
    for (auto i : G[u])
        if (i != fa)
        {
            dfs1(i, u);
            sz[u] += sz[i];
            if (sz[i] > sz[son[u]])
                son[u] = i;
        }
}

bool vis[N];
void add(int u, int fa, int op)
{
    if (op == 1)
        s[dep[u]].insert(p[u].first);
    else
        s[dep[u]].clear();
    for (auto i : G[u])
        if (i != fa && !vis[i])
            add(i, u, op);
}
void dfs2(int u, int fa, int op)
{
    for (auto i : G[u])
        if (i != fa && i != son[u])
            dfs2(i, u, 0);
    if (son[u])
        dfs2(son[u], u, 1), vis[son[u]] = 1;
    add(u, fa, 1);
    for (auto i : q[u])
        ans[i.first] = s[i.second + dep[u]].size();
    if (son[u])
        vis[son[u]] = 0;
    if (!op)
        add(u, fa, -1);
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin >> n;
    int top = 0;
    for (int i = 1; i <= n; ++i)
    {
        cin >> a[i] >> p[i].second;
        b[++top] = a[i];
    }
    sort(b + 1, b + top + 1);
    top = unique(b + 1, b + top + 1) - (b + 1);
    for (int i = 1; i <= n; ++i)
        p[i].first = lower_bound(b + 1, b + top + 1, a[i]) - b;
    int rt = 0;
    for (int i = 1; i <= n; ++i)
    {
        G[p[i].second].push_back(i);
        G[i].push_back(p[i].second);
    }
    dfs1(rt, 0);
    int m;
    cin >> m;
    for (int i = 1; i <= m; ++i)
    {
        int x, y;
        cin >> x >> y;
        q[x].push_back({i, y});
    }
    dfs2(rt, 0, 0);
    for (int i = 1; i <= m; ++i)
        cout << ans[i] << '\n';
    return 0;
}
View Code

4、codeforces208E Blood Cousins

题意:

给出一棵族谱树,给出一些询问,求和一个人有相同的第$k$级祖先的人有多少。

题解:

老$dsu$ $on$ $tree$了。但是这次是先求第$k$级祖先然后转化成上面第3题。其实这个$dsu$ $on$ $tree$在求解的时候,是在访问到求某个结点为根的子树的信息,这个第$k$级祖先就是根,而不能直接减一下深度然后放进存储询问的容器,这是初学者常犯的错误。所以我们就要写一个倍增预处理一下第$2^k$级祖先(当然你也可以长链剖分),这个时候再把询问存储好,然后就转化成了上题,只不过这里不用去重,所以不用$set$用$vector$。$vector$的$size$减一就是答案,注意,特判祖先不存在,此时输出$0$。

AC代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
vector<int> G[N];
int son[N], sz[N], dep[N];
vector<int> s[N << 1];
vector<pair<int, int>> q[N];
int ans[N];
bool vis[N];
int f[N][20];
void init(int n)
{
    for (int j = 1; j < 20; ++j)
        for (int i = 1; i <= n; ++i)
            f[i][j] = f[f[i][j - 1]][j - 1];
}
int getanc(int u, int p)
{
    for (int i = 19; ~i; --i)
        if ((1 << i) & p)
            u = f[u][i];
    return u;
}
void dfs1(int u, int fa)
{
    sz[u] = 1;
    if (u)
        dep[u] = dep[fa] + 1;
    for (auto i : G[u])
        if (i != fa)
        {
            dfs1(i, u);
            sz[u] += sz[i];
            if (sz[i] > sz[son[u]])
                son[u] = i;
        }
}
void add(int u, int fa, int op)
{
    if (op == 1)
        s[dep[u]].push_back(u);
    else
        s[dep[u]].clear();
    for (auto i : G[u])
        if (i != fa && !vis[i])
            add(i, u, op);
}
void dfs2(int u, int fa, int op)
{
    for (auto i : G[u])
        if (i != fa && i != son[u])
            dfs2(i, u, 0);
    if (son[u])
        dfs2(son[u], u, 1), vis[son[u]] = 1;
    add(u, fa, 1);
    for (auto i : q[u])
        ans[i.first] = s[dep[u] + i.second].size();
    if (son[u])
        vis[son[u]] = 0;
    if (!op)
        add(u, fa, 0);
}
int main()
{
    int n, a;
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
    {
        scanf("%d", &a);
        f[i][0] = a;
        G[a].push_back(i);
        G[i].push_back(a);
    }
    init(n);
    dfs1(0, 0);
    int m;
    scanf("%d", &m);
    for (int i = 1; i <= m; ++i)
    {
        int x, y;
        scanf("%d%d", &x, &y);
        if (dep[x] - y <= 0)
            ans[i] = 1;
        else
            q[getanc(x, y)].push_back({i, y});
    }
    dfs2(0, 0, 0);
    for (int i = 1; i <= m; ++i)
        printf("%d%c", ans[i] - 1, " \n"[i == m]);
    return 0;
}
View Code

*5、codeforces741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

题意:

给出一棵树,树上有一个字母作为边权,对于每一个结点,在以它为根的子树中,找到一条最长的路径,使得路径上的字母重排后可以组成一个回文串。字符集:$'a'-'v'$。

题解:

这个题是一道$2700$的题,刷新了我通过的题的难度等级,看到询问静态子树信息,继续是$dsu$ $on$ $tree$。但是这个子树信息就不好维护了,我们怎么维护这个路径呢?

首先我们先考虑怎么重排能变成回文串,显然至多只有一种字母出现奇数次。这样子,我们就可以$0$代表偶数次,$1$代表奇数次,又因为字符集大小只有$22$,所以表示这个字符串的状态只有$2^{22}$种,同时可以使用异或改变字符串状态。

然后无论如何这个字符串的两个端点一定存在一个$lca$。根据深度的性质,我们实际上是求出$max(dep[v]+dep[u]-2*dep[lca])$,因为我们显然不能一次求两个值(不然就是$O(n^2)$了),所以我们就要知道了一个的情况下求另一个。首先我们考虑一种简单的情况,就是这个字符串的其中一个端点就是两端点的$lca$,这样子我们就是直接往子树里面深搜更新最大值就完事了。

这样子我们可以大胆地思考一下一般情况。

posted @ 2020-04-17 00:24  Aya_Uchida  阅读(245)  评论(0编辑  收藏  举报