树链剖分(轻重链)

Posted zqytcl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树链剖分(轻重链)相关的知识,希望对你有一定的参考价值。

<前言>

树链剖分是我开始有点手熟的数据结构,未免遗忘,总结。

其他数据结构会一一补上,而且会多次修订,欢迎指教。

<更新档案>

1.None

完成博客编辑。

<正文>

树链剖分干的事其实很简单:把树进行以某个依据进行的拆分,放到数组上,这样就可以进行区间操作降低复杂度了。可以将链上操作、子树操作降为(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 到 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;
}

<后记>

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

以上是关于树链剖分(轻重链)的主要内容,如果未能解决你的问题,请参考以下文章

树链剖分

树链剖分详解

树链剖分

学习笔记::lct

树链剖分学习&BZOJ1036

树链剖分