• 树链剖分(轻重链)


    <更新档案>

    1.None

    完成博客编辑。

    2.<20201201>

    删去了不合时宜的前言。

    <正文>

    树链剖分干的事其实很简单:把树进行以某个依据进行的拆分,放到数组上,这样就可以进行区间操作降低复杂度了。可以将链上操作、子树操作降为(mathrm{O(log_2n)})级别。

    树链剖分可分为轻重链剖分长链剖分等,本文主要记录轻重链剖分相关内容。

    对于一棵树 :

    进行剖分之后长这样:

    放在数组上形如:

    | 1 3 7 9 | 10 | 6 | 2 5 8 | 4
    

    当然还有其他更加广泛的用途。

    本文记录基本操作。

    预处理

    我们需要预处理一些数组,具体如下:

    int top[N]; \ 每一个点对应的链的顶端
    int f[N];   \ 每个点的父节点
    int si[N], d[N]; \ 子树大小、节点深度
    int hs[N];  \ 每个点的最重儿子(子树最大)
    int tr[N], pr[N]; \ 树上点对应数组中点、 数组中点对应树上点
    

    其实也不多,每个都有效果。

    树链剖分的预处理需要两次DFS

    第一次处理子树深度、子树大小、父节点、重儿子, 这些都可以一次求出来。

    第二次处理链的顶端、数组与树的映射

    具体操作如下:

    void dfs(int u, int fa) {
        si[u] = 1;
        S_H(T, i, u) {
    	int v = T.to[i]; 
    	if (v == fa) continue;
    	f[v] = u, d[v] = d[u] + 1, dfs(v, u);
    	// 父节点、深度处理
            si[u] += si[v];
           	// 子树大小累加
            hs[u] = si[hs[u]] < si[v] ? v : hs[u];
            // 重儿子为子树大小最大的儿子;
        }
    }    // 第一次
    void dfs_chain(int u, int k) {
        top[pr[tr[u] = ++vs] = u] = k;
        // 建立映射, 处理链的顶端
        if (!hs[u]) return void();
        dfs_chain(hs[u], k);
        // 当前链会传递给重儿子
        S_H(T, i, u) {
    	int v = T.to[i];
    	if (v == f[u] || v == hs[u]) continue;
    	dfs_chain(v, v);
            // 轻儿子就要另起门户,自成链顶
        }
    }	 // 第二次
    

    LCA

    树剖求 LCA 其实到这步就好了,已经可以求 LCA 了。

    原理是:每次深度大的点跳到上一条链,直到两点在同一条链上,深度小的点就是 LCA,可以发现复杂度为 (mathrm{O(log_2n)})

    (mathrm{Code:})

    inline int Lca(int x, int y) {
        while(top[x] ^ top[y])
    	(d[top[x]] < d[top[y]] ? swap(x, y) : 0), x = f[top[x]];
        d[x] < d[y] ? swap(x, y) : 0;
        return y;
    }
    

    维护

    我们现在成功把树放在一条链上,接下来就要用数据结构维护了,一般选用线段树。

    线段树写在结构体 ( exttt{Segmentree}) 里,有一个变量 Se。

    链上修改&查询

    因为我们可以一次对一条重链上的所有点操作并做到 (mathrm{log_2n}), 我们可以每次操作之后往上跳一条重链。

    直到两点处于同一条重链,直接操作就好了。

    比如对于这个例子:

    如果我们要对 4 到 10 路径上的所有点进行 +1 的操作,则跳的过程如下:

    数组区间为:

    | 1 3 7 9 | 10 | 6 | 2 5 8 | 4
    
    1.  x, y = 4, 10;  对[5, 5] + 1; 10向上跳到7;
    2.  x, y = 4, 7 ;  对[10, 10] + 1; 4向上跳到2;
    3.  x, y = 2, 7 ;  对[7, 7] + 1;  2向上跳到1;
    4.  x, y = 1, 7 ;  对[1, 3] + 1;  操作结束;
    注意:if (d[top[x]] < d[top[y]]) swap(x, y);这句保证了操作顺序的正确性,使某个点不会被重复加两次。
    

    (mathrm{Code:})

    inline void Add_chain(int x, int y, int z) {
        while (top[x] ^ top[y]) {
    	d[top[x]] < d[top[y]] ? swap(x, y) : 0;
    	Se.Change(1, tr[top[x]], tr[x], 1, n, z), x = f[top[x]];
        }
        (d[x] < d[y] ? swap(x, y) : 0), Se.Change(1, tr[y], tr[x], 1, n, z);
        return void();
    }
    

    查询操作差不多,就是把线段树操作从修改变成了查询。

    (mathrm{Code:})

    inline ll Ask_chain(int x, int y) {
        ll sum = 0;
        while(top[x] ^ top[y]) {
    	if (d[top[x]] < d[top[y]]) swap(x, y);
            Add(sum, Se.Ask(1, tr[top[x]], tr[x], 1, n)), x = f[top[x]];
        }
        if (d[x] < d[y]) swap(x, y);
        return add(sum, Se.Ask(1, tr[y], tr[x], 1, n));
    }
    

    子树修改&查询

    Common Operation

    我们发现一个性质:子树上的点在数组上是一段连续的区间

    | 1 3 7 9 | 10 | 6 | 2 5 8 | 4
    

    你看:3 的子树在 [2, 6],5 的子树在 [8, 9]。

    如此,便可以直接操作了。操作基本类似于

    inline int Ask(int x) { return Se.Ask(1, tr[x], tr[x] + si[x] - 1, 1, n); }
    

    修改查询都类似。

    换根

    但是我们有时候需要考虑换根, 但是我们也不可能真的去换根,因为再做一遍预处理的时间复杂度有点吃不消。

    所以这时候需要用到小 Trick

    我们有三个固定点:查询点 x, 当前根 root, 原始根 1。

    考虑几种情况:

    • (x == root),此时 x 的子树既是整棵树,直接修改即可。

    • (Lca(x, root) != x), 此时 root ​不在 x 的以 1 为根的子树内,所以 x 的子树不变,按照上边的(mathrm{Common Operation})操作即可。

      形如:

    • (Lca(x, root) == x),此时(x)的子树为原本不在他子树内的所有点。我们先总体修改一遍,再减去不包括它的重儿子所在的子树。

      形如:

    此时 x 的子树为 所有点 - (以 x 为根,x 到 root 路径上里 x 最近一点的子树大小)。修改 & 查询时可以先对整棵树操作, 再在那个点(离 x 最近的点)的子树内逆向操作。

    (mathrm{Code:})

    inline int Getsub(int x, int y) {
        while (top[x] ^ top[y]) {
    	d[top[x]] < d[top[y]] ? swap(x, y) : 0;
    	if (f[top[x]] == y) return top[x];
    	x = f[top[x]];
        }
        d[x] < d[y] ? swap(x, y) : 0;
        return hs[y];
    }			// 寻找x到root路径上到x最近的点
    inline void Add_subtree(int x, int v) {
        if (x == root) return Se.Change(1, 1, n, 1, n, v);
        int lca = Lca(x, root);
        if (lca != x) return Se.Change(1, tr[x], tr[x] + si[x] - 1, 1, n, v);
        int fs = Getsub(x, root);
        Se.Change(1, 1, n, 1, n, v), Se.Change(1, tr[fs], tr[fs] + si[fs] - 1, 1, n, -v);
        return void();
    }			// 分类讨论换根,下同
    inline ll Ask_subtree(int x) {
        if (x == root) return Se.Ask(1, 1, n, 1, n);
        int lca = Lca(x, root);
        if (lca != x) return Se.Ask(1, tr[x], tr[x] + si[x] - 1, 1, n);
        int fs = Getsub(x, root);
        return Se.Ask(1, 1, n, 1, n) - Se.Ask(1, tr[fs], tr[fs] + si[fs] - 1, 1, n);
    }
    

    总结

    树链剖分的部分操作(链上操作)可以被倍增代替,所以说实话如果只有链上操作,写树剖有点难调。

    但是子树操作 + 链上操作是树剖比较有优势的。同时,树链剖分可以维护动态(dp)等算法,具体可见 UOJ、(mathrm{luogu})

    其实只要你肯写,很多树上操作题都可以用树剖实现。

    模板题完整代码, (mathrm{Code:})

    #include <climits>
    #include <iostream>
    #include <cstdio>
    #define rint register int
    typedef long long ll;
    #define FOR(i, a, b) for (rint i = (a); i <= (b); ++i)
    #define S_H(T, i, u) for (rint i = T.fl[u]; i; i = T.net[i])
    #define swap(x, y) (x ^= y ^= x ^= y)
    const int N = 1e5 + 10;
    int n, m, root;
    int a[N];
    
    inline ll add(ll a, ll b) {return a + b;}
    inline void Add(ll &a, ll b) {return a = add(a, b), void();}
    inline ll del(ll a, ll b) {return a - b;}
    inline void Del(ll &a, ll b) {return a = del(a, b), void();}
    struct Tree {
        int net[N << 1], to[N << 1];
        int fl[N], len;
        inline void inc(int x, int y) {
    	to[++len] = y;
    	net[len] = fl[x];
    	fl[x] = len;
        }
    } T;
    int si[N], hs[N], f[N], d[N];
    int top[N], tr[N], vs = 0, pr[N];
    struct Segmentree {
        class Point {public: ll sum, la;} t[N << 2];
        inline void Push(int p) {
            return t[p].sum = add(t[p << 1].sum, t[p << 1 | 1].sum), void();
        }
        void Build(int p, int l, int r) {
    	t[p].sum = t[p].la = 0;
    	if (l == r) {
    		t[p].sum = 1LL * pr[l];
    		return void();
    	}
    	int mid = (l + r) >> 1;
    	Build(p << 1, l, mid), Build(p << 1 | 1, mid + 1, r), Push(p);
        }
        inline void Update(int p, int ls, int rs) {
    	if (!t[p].la) return void();
    	Add(t[p << 1].la, t[p].la), Add(t[p << 1 | 1].la, t[p].la);
    	Add(t[p << 1].sum, t[p].la * ls), Add(t[p << 1 | 1].sum, t[p].la * rs);
    	t[p].la = 0;
    	return void();
        }
        inline void Change(int p, int l, int r, int x, int y, int v) {
    	if (l <= x && y <= r) {
    		Add(t[p].sum, 1LL * (y - x + 1) * v), Add(t[p].la, 1LL * v);
    		return void();
    	}
    	if (r < x || l > y) return void();
    	int mid = (x + y) >> 1;
    	Update(p, mid - x + 1, y - mid);
    	Change(p << 1, l, r, x, mid, v), Change(p << 1 | 1, l, r, mid + 1, y, v), Push(p);
        }
        inline ll Ask(int p, int l, int r, int x, int y) {
    	if (l <= x && y <= r) return t[p].sum;
    	if (r < x || l > y) return 0;
    	int mid = (x + y) >> 1;
    	Update(p, mid - x + 1, y - mid);
            return add(Ask(p << 1, l, r, x, mid), Ask(p << 1 | 1, l, r, mid + 1, y));
        }
    } Se;				//以上结构体线段树
    inline int read() {
        int s = 0, w = 1;
        char c = getchar();     
        while ((c < '0' || c > '9') && c != '-') c = getchar();
        if (c == '-') w = -1, c = getchar();
        while(c <= '9' && c >= '0') s = (s << 1) + (s << 3) + c - '0', c = getchar();
        return s * w;
    }
    template <class T>
    inline void write(T x) {     
        if (x < 0) x = ~x + 1, putchar('-');
        if (x > 9) write(x / 10);
        putchar(x % 10 + 48);
        return void();
    }
    // Definition
    void dfs(int u, int fa) {
        si[u] = 1;
        S_H(T, i, u) {
    	int v = T.to[i];
    	if (v == fa) continue;
    	f[v] = u, d[v] = d[u] + 1;
    	dfs(v, u);
    	si[u] += si[v];
    	hs[u] = si[hs[u]] < si[v] ? v : hs[u];
        }
    }
    void dfs_chain(int u, int k) {
        top[pr[tr[u] = ++vs] = u] = k, pr[vs] = a[u];
        if (!hs[u]) return void();
        dfs_chain(hs[u], k);
        S_H(T, i, u) {
    	int v = T.to[i];
    	if (v == f[u] || v == hs[u]) continue;
    	dfs_chain(v, v);
        }
    }
    // Preparation
    inline int Lca(int x, int y) {
        while(top[x] ^ top[y])
    	(d[top[x]] < d[top[y]] ? swap(x, y) : 0), x = f[top[x]];
        d[x] < d[y] ? swap(x, y) : 0;
        return y;
    }
    inline int Getsub(int x, int y) {
        while (top[x] ^ top[y]) {
    	d[top[x]] < d[top[y]] ? swap(x, y) : 0;
    	if (f[top[x]] == y) return top[x];
    	x = f[top[x]];
        }
        d[x] < d[y] ? swap(x, y) : 0;
        return hs[y];
    }
    inline void Add_chain(int x, int y, int z) {
        while (top[x] ^ top[y]) {
    	d[top[x]] < d[top[y]] ? swap(x, y) : 0;
    	Se.Change(1, tr[top[x]], tr[x], 1, n, z), x = f[top[x]];
        }
        (d[x] < d[y] ? swap(x, y) : 0), Se.Change(1, tr[y], tr[x], 1, n, z);
        return void();
    }
    inline void Add_subtree(int x, int v) {
        if (x == root) return Se.Change(1, 1, n, 1, n, v);
        int lca = Lca(x, root);
        if (lca != x) return Se.Change(1, tr[x], tr[x] + si[x] - 1, 1, n, v);
        int fs = Getsub(x, root);
        Se.Change(1, 1, n, 1, n, v), Se.Change(1, tr[fs], tr[fs] + si[fs] - 1, 1, n, -v);
        return void();
    }
    inline ll Ask_chain(int x, int y) {
        ll sum = 0;
         while(top[x] ^ top[y]) {
    	if (d[top[x]] < d[top[y]]) swap(x, y);
            Add(sum, Se.Ask(1, tr[top[x]], tr[x], 1, n)), x = f[top[x]];
        }
        if (d[x] < d[y]) swap(x, y);
        return add(sum, Se.Ask(1, tr[y], tr[x], 1, n));
    }
    inline ll Ask_subtree(int x) {
        if (x == root) return Se.Ask(1, 1, n, 1, n);
        int lca = Lca(x, root);
        if (lca != x) return Se.Ask(1, tr[x], tr[x] + si[x] - 1, 1, n);
        int fs = Getsub(x, root);
        return Se.Ask(1, 1, n, 1, n) - Se.Ask(1, tr[fs], tr[fs] + si[fs] - 1, 1, n);
    }
    // Operation
    signed main(void) {
        freopen("tree.in", "r", stdin);
        freopen("tree.out", "w", stdout);
        n = read(), root = 1;
        FOR(i, 1, n) a[i] = read();
        FOR(i, 1, n - 1) {
    	int x = read();
    	T.inc(i + 1, x), T.inc(x, i + 1);
        }
        dfs(root, 0), dfs_chain(root, root), Se.Build(1, 1, n);
        m = read();
        FOR(i, 1, m) {
    	int opt = read(), x = read();
    	if (opt == 1) root = x;
    	if (opt == 2) {
    		int y = read(), z = read();
    		Add_chain(x, y, z);
    	}
    	if (opt == 3) {
    		int z = read();
    		Add_subtree(x, z);
    	}
    	if (opt == 4) {
    		int y = read();
    		write(Ask_chain(x, y)), putchar(10);
    	}
    	if (opt == 5) write(Ask_subtree(x)), putchar(10);
    	// Working
        }
        return 0;
    }
    
    

    <后记>

    未完待续,还有一些内容需要补充。

  • 相关阅读:
    根据屏幕宽度适应屏幕样式
    设计模式之命令模式
    动态代理的使用以及其实现机制
    PLSQL链接不上oracle
    struts2自定义结果类型demo
    Tomcat虚拟路径
    SEQUENCE序列
    mysql导出数据库中建表语句和数据
    Tomcat6 启动时 Cannot assign requested address: JVM_Bind
    sql常用命令
  • 原文地址:https://www.cnblogs.com/yywxdgy/p/13204760.html
Copyright © 2020-2023  润新知