• 树链剖分学习笔记


    树链剖分,简称树剖,用来解决一类维护静态树上路径信息的问题。就是对树进行一些操作,使其变成线性的结构,从而用线段树进行维护。

    树链剖分的方法是轻重边剖分。我们将树上的边分成两种——轻边和重边。如果我们记siz[u]表示节点u的子树大小,在u的所有儿子之中,节点v的siz最大,则将边(u,v)称为重边,v称为u的重儿子。u到其他儿子的边就是轻边,其他的儿子就是轻儿子。

    对于轻重边有这样的性质:

      1.轻儿子的子树大小小于或等于其父亲子树大小的一半。即若(u,v)为轻边,满足siz[v] <= siz[u] / 2。

    证明其实很显然,假设存在一个轻儿子x使得siz[x] > siz[u] / 2,则u其他儿子siz之和都小于siz[x],那么x必然是重儿子,与假设矛盾,因此原命题成立。

      2.从根节点到某个节点v路径上的轻边个数不多于O(logn)。

    证明:显然如果是叶子结点可以满足边的数量最多,假设有一个叶子结点v满足路径上的边均为轻边,而由上一条性质可知,每次经过轻边实际上就是子树规模减少了一半,因此至多经过O(logn)条轻边就可以到达v。

      3.定义重路径为一条路径所有的边都为重边(特别地,一个点也是一条重路径)。则对于每个节点到根节点的路径上都有不超过O(logn)条轻边和O(logn)条重路径。这个由性质2显然可以得到证明,因为轻边已经不超过O(logn),除了轻边都是重路径,因此也不超过O(logn)。

    由以上三条性质可知,假如我们实现了对树进行轻重边剖分,那么每次处理路径复杂度都是O(logn)的,显然复杂度非常优秀。

    现在我们就需要考虑怎么实现轻重边剖分了。

    定义变量:

    链前存图:head[N],nxt[M],to[M],N为节点个数,M为边的数量。

    线段树相关:tree[N * 4],lazy[N * 4]

    树上信息:

      dep[N]维护节点到根节点距离。

      fa[N]维护节点父亲。

      siz[N]维护节点大小。

      son[N]维护节点重儿子编号。

      top[N]维护一条重路径上dep值最小的点的编号。

      dfn[N],pos[N]维护节点dfs序,dfn[x]为dfs序为x的节点编号,pos[u]为节点u对应的dfs序编号,二者互相对应。

      til[N]维护dfs序上子树大小,[ pos[u],til[u] ]即一段存储u子树的序列。

    好的相关变量定义完了(为什么这么多qwq)。

    首先我们需要进行一些dfs操作维护这些变量:

    第一次dfs,维护每个节点的父亲,深度,子树大小,重儿子编号。这个操作十分显然。

    void dfs1(int u)
    {
        dep[u] = dep[fa[u]] + 1;
        siz[u] = 1;
        for(int i = head[u];i;i = nxt[i])
        {
            int v = to[i];
            if(v == fa[u]) continue;
            fa[v] = u;
            dfs1(v);
            siz[u] += siz[v];
            if(siz[v] > siz[son[u]]) son[u] = v;//当一个儿子的siz比当前重儿子大就更新重儿子。
        }
        return;
    }

    第二次dfs,维护重路径上的top,dfs序,子树序列。这里一定要优先遍历重儿子,使得重路径在线段树中的位置是连续的。

    void dfs2(int u)
    {
        if(u == son[fa[u]]) top[u] = top[fa[u]];
        else top[u] = u;
        dfn[++tot] = u;
        pos[u] = tot;
        if(son[u]) dfs2(son[u]);
        for(int i = head[u];i;i = nxt[i])
        {
            int v = to[i];
            if(v == son[u] || v == fa[u]) continue;
            dfs2(v);
        }
        til[u] = tot;
        return;
    }

    这样两次dfs我们就成功维护好了重路径和树上信息了。然后就要进行路径修改操作了,通过递归实现。

    如果两个节点就在同一条重路径上,那么线段树中这两个点之间的区间也是连续的,直接修改就行。如果不在同一路径上,那么就需要进行“跳”的操作,也就是将深度较大的点向上跳,直接跳到当前重路径的顶端,然后继续进行这个操作直到两节点位于同一重路径。我们发现最坏情况下就是两个节点都需要跳到根节点处,而由前面论证可知这样跳的次数不会超过O(logn)次,每次进行一次线段树修改,复杂度O(logn),因此进行一次路径修改的总复杂度为O(log²n)。

    void path_change(int u,int v,int k)
    {
        if(top[u] == top[v]) 
        {
            if(pos[u] > pos[v]) swap(u,v);
            modify(1,1,n,pos[u],pos[v],k);
            return;
        }
        if(dep[top[u]] > dep[top[v]]) swap(u,v);
        modify(1,1,n,pos[top[v]],pos[v],k);
        path_change(u,fa[top[v]],k);
        return;
    }

     然后是路径查询,其实同理,只需要将路径修改中的修改部分改成查询就行,其实就是查询路径分成多条重路径上的答案然后求和。复杂度显然也是O(log²n)的。

    int path_query(int u,int v)
    {
        if(top[u] == top[v]) 
        {
            if(pos[u] > pos[v]) swap(u,v);
            return query(1,1,n,pos[u],pos[v]) % mod;
        }
        if(dep[top[u]] > dep[top[v]]) swap(u,v);
        return (query(1,1,n,pos[top[v]],pos[v]) + path_query(u,fa[top[v]])) % mod;
    }

    至于对子树进行修改,查询,只需要对[pos[x],til[x]]进行常规线段树修改查询操作就行了。

    贴一个洛谷P3384树链剖分的完整代码:

    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<iostream>
    #include<ctime>
    #include<cstdlib>
    #include<set>
    #include<queue>
    #include<vector>
    #include<string>
    using namespace std;
    
    #define P system("pause");
    #define A(x) cout << #x << " " << (x) << endl;
    #define AA(x,y) cout << #x << " " << (x) << #y << " " << (y) << endl;
    #define ll long long
    #define inf 1000000000
    #define linf 10000000000000000
    #define mem(x) memset(x,0,sizeof(x))
    
    int read()
    {
        int x = 0,f = 1;
        char c = getchar();
        while(c < '0' || c > '9')
        {
            if(c == '-') f = -1;
            c = getchar();
        }
        while(c >= '0' && c <= '9')
        {
            x = (x << 3) + (x << 1) + c - '0';
            c = getchar();
        }
        return f * x;
    }
    
    #define ls (x << 1),l,mid
    #define rs (x << 1 | 1),mid + 1,r
    #define mid ((l + r) >> 1)
    #define N 1000010
    #define M N << 1
    int tree[N << 2],lazy[N << 2],mod;
    int n,m,r,cnt;
    int head[N],nxt[M],to[M],val[N],fa[N],dep[N],siz[N],son[N],top[N];
    int dfn[N],pos[N],til[N],tot;
    void push_up(int x)
    {
        tree[x] = (tree[x << 1] + tree[x << 1 | 1]) % mod;
        return;
    }
    void build(int x,int l,int r)
    {
        if(l == r)
        {
            tree[x] = val[dfn[l]] % mod;
            return;
        }
        build(ls);
        build(rs);
        push_up(x);
        return;
    }
    void Add(int x,int l,int r,int k)
    {
        tree[x] = (1ll * tree[x] + (r - l + 1) * k) % mod;
        lazy[x] = (lazy[x] + k) % mod;
        return;
    }
    void push_down(int x,int l,int r)
    {
        if(!lazy[x]) return;
        Add(ls,lazy[x]);
        Add(rs,lazy[x]);
        lazy[x] = 0;
        return;
    }
    void modify(int x,int l,int r,int p,int q,int k)
    {
        if(p <= l && r <= q)
        {
            Add(x,l,r,k);
            return;
        }
        push_down(x,l,r);
        if(p <= mid) modify(ls,p,q,k);
        if(q > mid) modify(rs,p,q,k);
        push_up(x);
        return;
    }
    int query(int x,int l,int r,int p,int q)
    {
        if(p <= l && r <= q) return tree[x] % mod;
        int ret = 0;
        push_down(x,l,r);
        if(p <= mid) ret = (ret + query(ls,p,q)) % mod;
        if(q > mid) ret = (ret + query(rs,p,q)) % mod;
        return ret % mod;    
    }
    void add(int u,int v)
    {
        nxt[++cnt] = head[u];
        head[u] = cnt;
        to[cnt] = v;
    }
    void dfs1(int u)
    {
        dep[u] = dep[fa[u]] + 1;
        siz[u] = 1;
        for(int i = head[u];i;i = nxt[i])
        {
            int v = to[i];
            if(v == fa[u]) continue;
            fa[v] = u;
            dfs1(v);
            siz[u] += siz[v];
            if(siz[v] > siz[son[u]]) son[u] = v;
        }
        return;
    }
    void dfs2(int u)
    {
        if(u == son[fa[u]]) top[u] = top[fa[u]];
        else top[u] = u;
        dfn[++tot] = u;
        pos[u] = tot;
        if(son[u]) dfs2(son[u]);
        for(int i = head[u];i;i = nxt[i])
        {
            int v = to[i];
            if(v == son[u] || v == fa[u]) continue;
            dfs2(v);
        }
        til[u] = tot;
        return;
    }
    void path_change(int u,int v,int k)
    {
        if(top[u] == top[v]) 
        {
            if(pos[u] > pos[v]) swap(u,v);
            modify(1,1,n,pos[u],pos[v],k);
            return;
        }
        if(dep[top[u]] > dep[top[v]]) swap(u,v);
        modify(1,1,n,pos[top[v]],pos[v],k);
        path_change(u,fa[top[v]],k);
        return;
    }
    int path_query(int u,int v)
    {
        if(top[u] == top[v]) 
        {
            if(pos[u] > pos[v]) swap(u,v);
            return query(1,1,n,pos[u],pos[v]) % mod;
        }
        if(dep[top[u]] > dep[top[v]]) swap(u,v);
        return (query(1,1,n,pos[top[v]],pos[v]) + path_query(u,fa[top[v]])) % mod;
    }
    int main()
    {
        n = read(),m = read(),r = read(),mod = read();
        for(int i = 1;i <= n;i++) val[i] = read();
        for(int i = 1,u,v;i < n;i++)
        {
            u = read(),v = read();
            add(u,v);
            add(v,u);
        }
        dfs1(r);
        dfs2(r);
        build(1,1,n);
        int op,x,y,z;
        while(m--)
        {
            op = read(),x = read();
            switch(op)
            {
                case 1:
                    y = read(),z = read();
                    path_change(x,y,z);
                    break;
                case 2:
                    y = read();
                    printf("%d
    ",path_query(x,y) % mod);
                    break;
                case 3:
                    z = read();
                    modify(1,1,n,pos[x],til[x],z);
                    break;
                case 4:
                    printf("%d
    ",query(1,1,n,pos[x],til[x]) % mod);
                    break;
            }
        }
        return 0;
    }
    P3384完整代码

    好的树链剖分常用操作就完了,就是将树形结构变成线性结构处理,非常便捷,虽然码量不小,但其实并不难写。

     

  • 相关阅读:
    《深度剖析CPython解释器》25. 解密Python中的多线程(第一部分):初识GIL、以及多个线程之间的调度机制
    《深度剖析CPython解释器》24. Python运行时环境的初始化、源码分析Python解释器在启动时都做了哪些事情?
    《深度剖析CPython解释器》23. 剖析Python中模块的导入机制、Python是如何加载模块的
    《深度剖析CPython解释器》22. 解密Python中的生成器对象,从字节码的角度分析生成器的底层实现以及执行逻辑
    《深度剖析CPython解释器》21. Python类机制的深度解析(第五部分): 全方位介绍Python中的魔法方法,一网打尽
    《深度剖析CPython解释器》20. Python类机制的深度解析(第四部分): 实例对象的创建、以及属性访问
    redis 集群
    redis 主从
    文件的上传与下载
    Jsp
  • 原文地址:https://www.cnblogs.com/lijilai-oi/p/11261006.html
Copyright © 2020-2023  润新知