• 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 ? '
    ' : ' ');
        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
    " : "No
    ");
        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] << '
    ';
        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, " 
    "[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$,这样子我们就是直接往子树里面深搜更新最大值就完事了。

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

  • 相关阅读:
    好久没有写博客了
    老师网站 回顾及复习 https://www.linuxprobe.com/ (linux就该这么学 电子版)
    这周要考试了,还没有时间干其它的了,
    linux学习第十九天 (Linux就该这么学) 结课了
    linux学习第十八天 (Linux就该这么学)
    linux学习第十七天 (Linux就该这么学)
    Spanner's Correctness Constraints on Transactions
    Linearizability
    HDFS vs GFS
    Raft
  • 原文地址:https://www.cnblogs.com/Aya-Uchida/p/12716998.html
Copyright © 2020-2023  润新知