一类树上问题的解决办法

2018/07/23 16:48
阅读数 12

[TOC]

本文参考自 梁晏成《树上数据结构》 ,感谢他在雅礼集训的讲解。

转化成序列问题

dfs序

按照 $dfs$ 的入栈顺序形成一个序列。

例如对于这棵树

img

它的 $dfs$ 序就是 $1~2~3~4~5~6~7~8$ 。(假设我遍历儿子是从左到右的)

树链剖分的运用

对于这个我们常常配合 树链剖分 来使用。

这样对于一个点,它的子树编号是连续的一段区间,便于做子树修改以及查询问题。

重链上所有节点的标号也是连续的一段区间。

所以我们可以解决大部分链或子树修改以及查询的问题,十分的优秀。

也就是常常把树上问题转化成序列问题的通用解法。

括号序列

$dfs$ 时候,某个节点入栈时加入左括号,出栈时加入右括号。

也就是在 $dfs$ 序旁边添加括号。

同样对于上面那颗树 。

为了方便观看,我们在其中添入一些数字。

它的括号序列就是 $(1(2)(3(4)(5(6(7))))(8))$ 。

求解树上距离问题

这个可以对于一些有关于树上距离的问题有用,比如 BZOJ1095 [ZJOI2007] Hide 捉迷藏 (括号序列 + 线段树)

也就是对于树上两点的距离,就是他们中间未匹配的括号数量。这个是很显然的,因为匹配的括号必定不存在于他们之间的路径上,其他的都存在于他们的路径上。

也就是说向上路径的括号是 $)$ 向下路径的括号就是 $($ 。

树上莫队转化成普通莫队

令 $L_x$ 为 $x$ 左括号所在的位置,$R_x$ 为 $x$ 右括号所在的位置。

我们查询树上一条路径 $x \sim y$ 满足 $L_x \le L_y$ ,考虑:

  • 如果 $x$ 是 $y$ 的祖先,那么 $x$ 到 $y$ 的链与括号序列 $[L_x, L_y]$ 对应。
  • 如果 $x$ 不是 $y$ 的祖先,那么 $x$ 到 $y$ 的链除 $lca$ 部分与括号序列中区间 $[R_x, L_y]$ 对应。

第二点是因为 $lca$ 的贡献会在其中被抵消掉,最后暴力算上就行了。

每次移动的时候就修改时候判断一个点被匹配了没,匹配减去,没匹配加上就行了。

SP10707 COT2 - Count on a tree II

题意

多次询问树上一条路径上不同颜色种数。

题解

我们利用括号序列,把树上的问题直接拍到序列上来做暴力莫队就行了,和之前莫队模板题一样的做法。

欧拉序列

$dfs$ 时,某个节点入栈时加入队列,出栈时将父亲加入队列。

还是对于上面那颗树,

它的欧拉序列就是 $1~2~1~3~4~3~5~6~7~6~5~3~1~8~1$ 。

这个有什么用呢qwq 常常用来做 $lca$ 问题。

具体来说就是,对于欧拉序列每个点记住它的深度,然后对于任意两个点的 $lca$ 就是他们两个点第一次出现时候的点对之间 深度最小 的那个点。

这就转化成了一个 $RMQ$ 问题,用普通的 $ST$ 表预处理就可以达到 $O(n \log n)$ ,询问就是 $O(1)$ 的。

如果考虑用约束 $RMQ$ 来解决,就可以达到 $O(n)$ 预处理,$O(1)$ 询问的复杂度。

虽然看起来特别优秀,但是并不常用qwq

差分思想

  • 对于一对点 $x, y$ ,假设它们 $lca$ 为 $z$ ,那么这条 $x$ 到 $y$ 的链可以用 $x, y, z, fa[z]$ 的链表示。

    例如给一条 $x \to y$ 的链加上一个数 $v$ ,最后询问每个点的权值。

    我们可以把 $x,y$ 处加上 $v$ ,$z, fa[z]$ 处减去 $v$ ,最后对于每个点求子树和就是这个点的权值了。

    注意要特判 $lca = x ~ or ~ y$ 的情况。

  • 对于两条相同的边上的信息可以抵消(链上所有边异或的值),可以直接拆成 $x, y$ 到根的路径表示。

单点、链、子树的转化

在某些情况下,我们需要修改和改变查询的对象来减小维护的难度。

下面我都把链看成从底向上的一条,其他链其实都可以拆分成两条这种链(一条 $x \to lca$ 向上,另一条 $lca \to x$ 向下),也可以类比接下来的方法进行讨论。

  • 单点修改链上查询 $\Leftrightarrow$ 子树修改单点查询

    这个如何理解呢,例如对于这颗树。

    img

    我们考虑对于修改 $x$ 的点权值,不难发现它影响的链就是类似 $y,z \to anc[x]$ ( $x$ 自己 以及 它的祖先)的点。

    然后就可以在 $x$ 处给子树修改权值,每次查询一条链就是看它链底的权值和减去链顶的权值和。

    反过来也是差不多的思路。

  • 链上修改单点查询 $\Leftrightarrow$ 单点修改子树查询

    $y \to x$ 这条链上修改权值,查询一个点的权值。

    不难发现,这就等价于给 $x, y$ 处打差分标记,然后每次查询一颗子树的信息。

    这样的话,对于一个点所包含的子树信息,就是整个所有之前链覆盖它的信息。

    这个常常可以用于最后询问很多个点,然后用线段树合并子树信息。

  • 链上修改子树查询 $\Leftrightarrow$ 单点修改子树查询

    似乎是利用 $dep$ 数组实现的,不太记得怎么搞了,以后做了题再来解释吧。

点、边

一些与“链相交”的问题,我们可以在点上赋正权,边上赋负权的方式简化问题。

例题

题意

  • 插入一条链
  • 给定一条链,问有多少条链于这条链相交。

题解

我们只需要在插入的时候,给链上的点 $+1$ ,链上的边 $-1$ ,询问的时候就等价于一个链上求和。

这为什么是正确的呢?对于两条链,我们把负的边权和下面正的点权抵消掉,那么就只剩下了最上面共有的交点有多的 $1$ 的贡献了。

提取关键点

我们可以在一棵树中取不超过 $\sqrt n$ 个关键点,保证每个点到最近的祖先距离 $\le \sqrt n$ 。

具体地,我们自底向上标记关键点。如果当前点子树内到它最远的点距离 $\ge \sqrt n$ 就把当前点标记成关键点。

其实类似于序列上的分块处理。

HDU 6271 Master of Connected Component

题意

给定两颗 $n$ 个节点的树,每个节点有一对数 $(x, y)$ ,表示图 $G$ 中的一条边。

对于每一个 $x$ ,求出两棵树 $x$ 到根路径上的所有边在图 $G$ 中构成的子图联通块个数。

多组数据,$n \le 10000$ 。

题解

考虑对于第一颗树提取关键点,然后对于每个点的询问挂在它最近的关键点祖先处。

到每个关键点处理它所拥有的询问,到第二颗树上进行遍历,遍历到一个当前关键点所管辖的节点的时刻就处理它在第一棵树的信息。

对于一个关键点 $p$ 将它到根节点路径上的节点全部放入并查集中,然后用支持撤回的并查集维护联通块个数。

具体来说,对于那个撤回并查集只需要按秩合并,也就是深度小的连到深度大的上,然后记一下上次操作的深度,以及连的边。

不难发现每个点在第一棵数上只会更新到被管辖关键点的距离,这个只有 $\mathcal O(\sqrt n)$ 。然后第二棵树同时也只会被遍历 $\mathcal O(\sqrt n)$ 次。

然后这个时间复杂度就是 $O(n \sqrt n \log n)$ 的,其实跑的很快?

代码

强烈建议认真阅读代码,提高码力。

#include <bits/stdc++.h>

#define For(i, l, r) for(int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}

void File() {
#ifdef zjp_shadow
	freopen ("6271.in", "r", stdin);
	freopen ("6271.out", "w", stdout);
#endif
}

const int N = 2e4 + 50, M = N * 2, blksize = 350;

typedef pair<int, int> PII;
#define fir first
#define sec second
#define mp make_pair

struct Data {
	
	int x, y, type; 

	Data() {}

	Data(int a, int b, int c) : x(a), y(b), type(c) {}

} opt[N];

namespace Union_Set {

	int fa[N]; int Find(int x) { return x == fa[x] ? x : Find(fa[x]); }

	int height[N], tot = 0;
	inline Data Merge(int x, int y){
		int rtx = Find(x), rty = Find(y);
		if (rtx == rty) return Data(0, 0, 0);
		if (height[rtx] < height[rty]) swap(rtx, rty);
		fa[rty] = rtx; -- tot;
		if (height[rtx] == height[rty]) { ++ height[rtx]; return Data(rtx, rty, 2); }
		else return Data(rtx, rty, 1);
	}

	inline void Retract(Data now) {
		int x = now.x, y = now.y, type = now.type;
		if (!type) return ; height[x] -= (type - 1); fa[y] = y; ++ tot;
	}

}

PII Info[N];
inline Data Insert(int pos) {
	int x = Info[pos].fir, y = Info[pos].sec;
	return Union_Set :: Merge(x, y);
}

inline void Delete(int pos) {
	Union_Set :: Retract(opt[pos]); 
}

int from[N], nowrt;
inline int Get_Ans(int u) {
	static int stk[N], top; top = 0;
	while (u ^ nowrt) {
		opt[u] = Insert(u), stk[++ top] = u, u = from[u];
	}
	int res = Union_Set :: tot;
	while (top) Delete(stk[top --]);
	return res;
}

int Head[N], Next[M], to[M], e;
void add_edge(int u, int v) { to[++ e] = v; Next[e] = Head[u]; Head[u] = e; }

int maxd[N], vis[N];

#define Travel(i, u, v) for(int i = Head[u], v = to[i]; i; i = Next[i], v = to[i])
void Dfs_Init(int u, int fa = 0) {
	from[u] = fa; maxd[u] = 1;
	Travel(i, u, v) if (v != fa) {
		Dfs_Init(v, u);
		chkmax(maxd[u], maxd[v] + 1);
	}
	if (maxd[u] == blksize || u == 1) maxd[u] = 0, vis[u] = true;
}

int n, m;

vector<int> child[N];
inline bool App(int u) {
	vector<int> :: const_iterator it = lower_bound(child[nowrt].begin(), child[nowrt].end(), u);
	if (it == child[nowrt].end()) return false; return (*it == u);
}

int ans[N];
void Dfs2(int u, int fa = 0) {
	opt[u] = Insert(u);
	if (App(u - n)) ans[u - n] = Get_Ans(u - n);
	Travel(i, u, v) if (v != fa) Dfs2(v, u);
	Delete(u);
}

void Dfs1(int u, int fa = 0) {
	opt[u] = Insert(u);
	if (vis[u]) nowrt = u, Dfs2(n + 1, 0);
	Travel(i, u, v) if (v != fa) Dfs1(v, u);
	Delete(u);
}

inline void Init() {
	e = 0; 
	For (i, 1, n * 2) 
		from[i] = 0, Head[i] = 0, child[i].clear(), vis[i] = false;
	For (i, 1, m)
		Union_Set :: fa[i] = i, Union_Set :: height[i] = 1;
	Union_Set :: tot = m;
}

int main () {
	File();

	for (int cases = read(); cases; -- cases) {
		n = read(); m = read(); Init();

		For (id, 0, 1) {
			For (i, 1, n)
				Info[i + id * n] = mp(read(), read());
			For (i, 1, n - 1) {
				int u = read() + id * n, v = read() + id * n;
				add_edge(u, v); add_edge(v, u);
			}
			Dfs_Init(1 + id * n);
		}

		For (i, 1, n) {
			int u = i;
			for (; !vis[u]; u = from[u]) ;
			child[u].push_back(i);
		}

		Dfs1(1); For (i, 1, n) printf ("%d\n", ans[i]); Init();
	}

	return 0;
}

启发式合并

启发式合并即合并两个集合时按照一定顺序(通常是将较小的集合的元素一个个插入较大的集合)合并的一种合并方式,常见的数据结构有并查集、平衡树、堆、字典树等。

具体地,如果单次合并的复杂度为 $O(B)$ ,总共有 $M$ 个信息,那么总复杂度为 $O(B M \log M)$ 。

树的特殊结构,决定了常常可以使用启发式合并优化信息合并的速度。

LOJ #2107. 「JLOI2015」城池攻占

此处例题有很多,就放一个还行的题目上来。

题意

请点下上面的链接,太长了不想写了。

题解

不难发现两个骑士经过同一个节点的时候,攻击力的相对大小是不会改变的;

然后我们每次找当前攻击力最小的骑士出来,判断是否会死亡。

这个可以用一个可并小根堆实现(也可以用 splay 或者 treap 各类平衡树实现)。

我们可以用 lazy 标记来支持加法和乘法操作就行了。

用斜堆实现似乎常数比左偏树小?还少了一行qwq

并且斜堆中每个元素的下标就是对应着骑士的编号,很好写!

复杂度是 $O(m \log m)$ ,lych 说是 $O(m \log ^ 2 m)$ ? 我也不知道是不是qwq

代码

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

typedef long long ll;
inline ll read() {
	ll x = 0, fh = 1; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
	return x * fh;
}

void File() {
#ifdef zjp_shadow
	freopen ("2107.in", "r", stdin);
	freopen ("2107.out", "w", stdout);
#endif
}

const int N = 3e5 + 1e3;
const ll inf = 1e18;

int n, m; ll Def[N];
int opt[N]; ll val[N];

namespace Lifist_Tree {

	ll val[N], TagMult[N], TagAdd[N];

	int ls[N], rs[N];

	inline void Mult(int pos, ll uv) { if (pos) val[pos] *= uv, TagAdd[pos] *= uv, TagMult[pos] *= uv; }

	inline void Add(int pos, ll uv) { if (pos) val[pos] += uv, TagAdd[pos] += uv; }

	inline void Push_Down(int x) {
		if (TagMult[x] != 1)
			Mult(ls[x], TagMult[x]), Mult(rs[x], TagMult[x]), TagMult[x] = 1;

		if (TagAdd[x] != 0)
			Add(ls[x], TagAdd[x]), Add(rs[x], TagAdd[x]), TagAdd[x] = 0;
	}

	int Merge(int x, int y) {
		if (!x || !y) return x | y;
		if (val[x] > val[y]) swap(x, y);
		Push_Down(x); 
		rs[x] = Merge(rs[x], y);
		swap(ls[x], rs[x]);
		return x;
	}

	inline int Pop(int x) {
		Push_Down(x);
		int tmp = Merge(ls[x], rs[x]);
		ls[x] = rs[x] = 0;
		return tmp;
	}

}

vector<int> G[N];
int dep[N], die[N], ans[N], rt[N];
void Dfs(int u) {
	int cur = rt[u];
	for (int v : G[u])
		dep[v] = dep[u] + 1, Dfs(v), cur = Lifist_Tree :: Merge(cur, rt[v]);

	while (cur && Lifist_Tree :: val[cur] < Def[u])
		die[cur] = u, cur = Lifist_Tree :: Pop(cur), ++ ans[u];
	if (opt[u])
		Lifist_Tree :: Mult(cur, val[u]);
	else
		Lifist_Tree :: Add(cur, val[u]);

	rt[u] = cur;
}

int pos[N];
int main () {

	File();

	n = read(); m = read();

	Def[0] = inf; For (i, 1, n) Def[i] = read();

	For (i, 2, n) {
		int from = read();
		G[from].push_back(i);
		opt[i] = read(); val[i] = read();
	}
	G[0].push_back(1);

	For (i, 1, m) {
		Lifist_Tree :: val[i] = read(); Lifist_Tree :: TagMult[i] = 1; pos[i] = read();
		rt[pos[i]] = Lifist_Tree :: Merge(rt[pos[i]], i);
	}

	Dfs(0);
	For (i, 1, n) 
		printf ("%d\n", ans[i]);
	For (i, 1, m)
		printf ("%d\n", dep[pos[i]] - dep[die[i]]);

	return 0;
}

直径的性质

令 $F(S)$ 表示集合 $S$ 中最远的两个点构成的集合,那么对同一棵树中的集合 $S, T$ ,$F(S \cup T) \subseteq F(S) \cup F(T)$ 。

这个证明。。。我不会qwq fakesky 说可以反证法来证明?

51nod 1766 树上最远点对

题意

给定一棵树,多次询问 $a, b, c, d$ ,求 $\displaystyle \max_{a \le i \le b, c \le j \le d} dist(i, j)$ 。

题解

用线段树维护区间最远点对,然后利用上面的性质。

每次合并的时候枚举 $\displaystyle\binom 4 2 = 6$ 种情况,取最远的一对作为答案就行了。

用前面讲的欧拉序列和 $ST$ 表求 $lca$ ,复杂度可以优化成 $O((n + q) \log n)$ 。

代码

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1; for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48); return x * fh; }

void File() {
    freopen ("1766.in", "r", stdin);
    freopen ("1766.out", "w", stdout);
}

const int N = 110000;

typedef pair<int, int> PII;
#define fir first
#define sec second

vector<PII> G[N];
int dep[N], dis[N], minpos[N * 2][21], tot = 0, Log2[N * 2], app[N];

inline bool cmp(int x, int y) { return dep[x] < dep[y]; }

inline int Get_Lca(int x, int y) {
	int len = Log2[y - x + 1], 
		p1 = minpos[x][len], 
		p2 = minpos[y - (1 << len) + 1][len];
	return cmp(p1, p2) ? p1 : p2;
}

inline int Get_Dis(int x, int y) {
	int tmpx = app[x], tmpy = app[y];
	if (tmpx > tmpy) swap(tmpx, tmpy);
	int Lca = Get_Lca(tmpx, tmpy);
	return dis[x] + dis[y] - dis[Lca] * 2;
}

void Dfs_Init(int u, int fa = 0) {
	minpos[app[u] = ++ tot][0] = u;
    dep[u] = dep[fa] + 1;
	For (i, 0, G[u].size() - 1) {
		PII cur = G[u][i];
		int v = cur.fir;
		if (v != fa) dis[v] = dis[u] + cur.sec, Dfs_Init(v, u);
	}
	if (fa) minpos[++ tot][0] = fa;
}

typedef pair<int, int> PII;
#define fir first
#define sec second
#define mp make_pair

inline void Update(PII &cur, PII a, PII b, bool flag) {
	int lx = a.fir, ly = a.sec, rx = b.fir, ry = b.sec, res = 0;

	if (flag && chkmax(res, Get_Dis(lx, ly))) cur = mp(lx, ly);
	if (chkmax(res, Get_Dis(lx, rx))) cur = mp(lx, rx);
	if (chkmax(res, Get_Dis(lx, ry))) cur = mp(lx, ry);

	if (chkmax(res, Get_Dis(ly, rx))) cur = mp(ly, rx);
	if (chkmax(res, Get_Dis(ly, ry))) cur = mp(ly, ry);
	if (flag && chkmax(res, Get_Dis(rx, ry))) cur = mp(rx, ry);
}

namespace Segment_Tree {

#define lson o << 1, l, mid
#define rson o << 1 | 1, mid + 1, r

	PII Adv[N << 2];

	void Build(int o, int l, int r) {
		if (l == r) { Adv[o] = mp(l, r); return ; }
		int mid = (l + r) >> 1;
		Build(lson); Build(rson);
		Update(Adv[o], Adv[o << 1], Adv[o << 1 | 1], true);
	}

	PII Query(int o, int l, int r, int ql, int qr) {
		if (ql <= l && r <= qr) return Adv[o];
		PII tmp; int mid = (l + r) >> 1;
		if (qr <= mid) tmp = Query(lson, ql, qr);
		else if (ql > mid) tmp = Query(rson, ql, qr);
		else Update(tmp, Query(lson, ql, qr), Query(rson, ql, qr), true);
		return tmp;
	}

#undef lson
#undef rson

}

int n, m;

int main () {

	n = read();
	For (i, 1, n - 1) {
		int u = read(), v = read(), w = read();
		G[u].push_back(mp(v, w));
		G[v].push_back(mp(u, w));
	}
	Dfs_Init(1);

	For (i, 2, tot) Log2[i] = Log2[i >> 1] + 1;

	For (j, 1, Log2[tot]) For (i, 1, tot - (1 << j) + 1) {
		register int p1 = minpos[i][j - 1], p2 = minpos[i + (1 << (j - 1))][j - 1];
		minpos[i][j] = cmp(p1, p2) ? p1 : p2;
	}


	Segment_Tree :: Build(1, 1, n);

	m = read();
	For (i, 1, m) {
		int a = read(), b = read(), c = read(), d = read();

		PII ans;

		Update(ans, 
				Segment_Tree :: Query(1, 1, n, a, b), 
				Segment_Tree :: Query(1, 1, n, c, d), false);

		printf ("%d\n", Get_Dis(ans.fir, ans.sec));
	}

	return 0;
}

雅礼NOIp 7-22 Practice

题意

给你一棵以 $1$ 为根的树,一开始所有点全为黑色。

需要支持两个操作:

  • $C ~ p$ ,将 $p$ 节点反色
  • $G ~ p$ ,求 $p$ 子树中最远的两个黑色节点的距离。

题解

[ZJOI2007] 捉迷藏 进行了加强,支持查询子树。

和上面那题是一样的,因为每棵树的子树的 $dfs$ 序是连续的。

我们考虑用线段树维护一段连续 $dfs$ 序的点的最远点对就行了。

长链剖分

把重链剖分中重儿子的定义变成子树内叶子深度最大的儿子,就是长链剖分了。

但为什么我们做链上操作的时候不用长链剖分呢?因为一个点到根的轻边个数可以是 $O(\sqrt n)$ 的级别,如图:

img

k-th ancestor

用这个可以实现 $O(n \log n)$ 预处理, $O(1)$ 查询一个点 $x$ 的 $k$ 级祖先。

具体实现参考这个 Bill Yang 大佬的博客 讲的挺好的qwq

O(n) 统计每个点子树中以深度为下标的可合并信息

具体来说就是巧妙的继承重儿子状态,把自己的状态 $O(1)$ 添加在最后,然后暴力按其他轻儿子重链长度继承状态,就行了。复杂度是 $O(\sum$ 重链长 $) = O(n)$ 的。

BZOJ 3653: 谈笑风生

点进去就行啦,网上唯一一篇长链剖分的题解。。(真不要脸)

定长最长链

给定一棵树,求长度为 $L$ 的边权和最大的链。

对点 $x$ ,设重链长为 $l$ ,维护 $f_{x, 1..l}$ 表示以 $x$ 为根长度为 $1 .. l$ 的链最大的边权和。

每次直接继承重儿子的 $f$ ,然后和轻儿子依次合并,合并的时候顺便计算就行了。

时间复杂度 $O(n)$ 。

树链剖分维护动态 dp

动态修改边权,维护直径

令 $f_x, g_x$ 表示以 $x$ 为根的最长链和 $x$ 子树内直径的长度,令 $y$ 为 $x$ 的儿子,每次用 $(f_y + 1, \max{f_x + f_y + 1, g_y})$ 来更新 $(f_x, g_x)$ 。

每次考虑优先转移轻儿子,最后再来转移重儿子。令 $f', g'$ 表示转移完轻儿子的结果,那么每次只会修改 $O(\log )$ 个 $f', g'$ ,可以暴力处理;重链上的转移可以维护类似 $a_i = \max {a_{i+1} + A, B}$ 的标记 $A, B$ 。使用线段树合并,也可以使用矩阵来做。总复杂度 $O(m \log^2 n)$ 常数有点大。

动态修改点权,询问最大权独立集

这个直接点进去看我博客就行啦,继续不要脸一波。。。

link-cut-tree

这个是个解决大量树上(甚至图上)问题的利器。可以见我之前的博客讲解

LOJ #2001. 「SDOI2017」树点涂色

题意

直接点上面的链接,题意很清楚啦qwq

题解

首先解决链的答案,考虑差分,$ans = ans_u + ans_v - 2 \times ans_{lca(u, v)} + 1$ 。

这个证明可以分两种情况讨论,一种 $lca$ 和下面的点有一样的颜色,另一种没有,其实都是一样的情况。

只是要注意每次染上的都是不同的颜色,所以满足。

每次把一个点到根染上一种新颜色,不难发现这很类似于 $lct$ 中的 $Access$ 操作。

具体来说,$lct$ 每个 $splay$ 维护的是一个相同颜色的集合。

每次涂新颜色,不难发现就是 $Access$ 断开的右儿子 $splay$ 的根所在的子树答案会增加 $1$ ,新接上去的儿子需要减 $1$ 。

然后我们需要子树加,子树查 $\max$ ,这个直接用 树剖 + 线段树 就可以维护了。

代码

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}

void File() {
#ifdef zjp_shadow
	freopen ("2001.in", "r", stdin);
	freopen ("2001.out", "w", stdout);
#endif
}

const int Maxn = 1e5 + 1e3, N = 1e5 + 1e3;


int n, m;
vector<int> G[N]; 

int fa[N], sz[N], dep[N], son[N];
void Dfs_Init(int u, int from = 0) {
	dep[u] = dep[fa[u] = from] + 1; sz[u] = 1;
	for (int v : G[u]) if (v != from) {
		Dfs_Init(v, u); 
		sz[u] += sz[v];
		if (sz[son[u]] < sz[v]) son[u] = v;
	}
}
 
int dfn[N], num[N], top[N];
void Dfs_Part(int u) {
	static int clk = 0;
	num[dfn[u] = ++ clk] = u;
	top[u] = son[fa[u]] == u ? top[fa[u]] : u;
	if (son[u]) Dfs_Part(son[u]);
	for (int v : G[u]) if (v != fa[u] && v != son[u]) Dfs_Part(v);
}

namespace Segment_Tree {

#define lson o << 1, l, mid
#define rson o << 1 | 1, mid + 1, r

	int maxv[N << 2], Tag[N << 2];

	inline void Add(int o, int uv) {
		maxv[o] += uv; Tag[o] += uv; 
	}
	
	inline void Push_Down(int o) {
		if (!Tag[o]) return ;
		Add(o << 1, Tag[o]); Add(o << 1 | 1, Tag[o]); Tag[o] = 0;
	}

	inline void Push_Up(int o) {
		maxv[o] = max(maxv[o << 1], maxv[o << 1 | 1]);
	}

	void Build(int o, int l, int r) {
		if (l == r) { maxv[o] = dep[num[l]]; return ; }
		int mid = (l + r) >> 1; Build(lson); Build(rson); Push_Up(o);
	}

	void Update(int o, int l, int r, int ul, int ur, int uv) {
		if (ul <= l && r <= ur) { Add(o, uv); return ; }
		int mid = (l + r) >> 1; Push_Down(o);
		if (ul <= mid) Update(lson, ul, ur, uv);
		if (ur > mid) Update(rson, ul, ur, uv); Push_Up(o);
	}

	int Query(int o, int l, int r, int ql, int qr) {
		if (ql <= l && r <= qr) return maxv[o];
		int tmp = 0, mid = (l + r) >> 1; Push_Down(o);
		if (ql <= mid) chkmax(tmp, Query(lson, ql, qr));
		if (qr > mid) chkmax(tmp, Query(rson, ql, qr));
		Push_Up(o); return tmp;
	}

#undef lson
#undef rson

}

namespace Link_Cut_Tree {

#define ls(o) ch[o][0]
#define rs(o) ch[o][1]

	int fa[Maxn], ch[Maxn][2];

	inline bool is_root(int o) {
		return o != ls(fa[o]) && o != rs(fa[o]);
	}

	inline bool get(int o) { return o == rs(fa[o]); }

	inline void Rotate(int v) {
		int u = fa[v], t = fa[u], d = get(v);
		fa[ch[u][d] = ch[v][d ^ 1]] = u;
		fa[v] = t; if (!is_root(u)) ch[t][rs(t) == u] = v;
		fa[ch[v][d ^ 1] = u] = v;
	}

	inline void Splay(int o) {
		for (; !is_root(o); Rotate(o)) 
			if (!is_root(fa[o])) 
				Rotate(get(o) ^ get(fa[o]) ? o : fa[o]);
	}

	inline int Find_Root(int o) {
		while (ls(o)) o = ls(o); return o;
	}

	inline void Access(int o) {
		for (register int t = 0, rt; o; o = fa[t = o]) {
			Splay(o); 
			if (rs(o)) rt = Find_Root(rs(o)), Segment_Tree :: Update(1, 1, n, dfn[rt], dfn[rt] + sz[rt] - 1, 1);
			rs(o) = t;
			if (rs(o)) rt = Find_Root(rs(o)), Segment_Tree :: Update(1, 1, n, dfn[rt], dfn[rt] + sz[rt] - 1, - 1);
		}
	}

}

inline int Get_Lca(int x, int y) {
	for (; top[x] ^ top[y]; x = fa[top[x]])
		if (dep[top[x]] < dep[top[y]]) swap(x, y);
	return dep[x] < dep[y] ? x : y;
}

int main () {

	File();

	n = read(); m = read();
	For (i, 1, n - 1) {
		int u = read(), v = read();
		G[u].push_back(v);
		G[v].push_back(u);
	}
	Dfs_Init(1); Dfs_Part(1);
	Segment_Tree :: Build(1, 1, n);

	For (i, 1, n)
		Link_Cut_Tree :: fa[i] = fa[i];

	For (i, 1, m) {
		int opt = read();
		if (opt == 1) {
			int pos = read();
			Link_Cut_Tree :: Access(pos);
		}
		if (opt == 2) {
			int x = read(), y = read(), Lca = Get_Lca(x, y);
			printf ("%d\n", 
					Segment_Tree :: Query(1, 1, n, dfn[x], dfn[x]) + Segment_Tree :: Query(1, 1, n, dfn[y], dfn[y]) - 
					2 * Segment_Tree :: Query(1, 1, n, dfn[Lca], dfn[Lca]) + 1);
		}
		if (opt == 3) {
			int pos = read();
			printf ("%d\n", Segment_Tree :: Query(1, 1, n, dfn[pos], dfn[pos] + sz[pos] - 1));
		}
	}

    return 0;
}

维护 MST

利用 $lct$ 可以维护只有插入的 $MST$ 。

为了方便,拆边为点,也就是说 $x \to y$ 变成 $x \to z \to y$ ,将边权变成点权。

每次只要支持查找一条路径上边权最大的边,以及删边和加边就行了。

维护图的连通性

如果允许离线,那么可以实现利用 $lct$ 维护图的连通性的有关信息。

根据贪心的思想,我们希望保留尽量晚被删除的边。于是可以考虑维护以删除时间为权值的最大生成森林,和上面那个方法就是一样的。

如果一个图 $G$ 存在补图 $G'$ ,可以考虑同时维护图 $G$ 和 $G'$ 的两个生成森林 $T$ 和 $T'$ ,在 $G$ 中删边相当于在 $G'$ 中加边,这样可以解决 $lct$ 难以实现删边的问题。

点分治

树的重心

一般情况下,如果一个点满足它作为根时最大子树的大小最小,我们就称这个点为树的重心。

应用

点分治是将当前子树的重心作为分治中心的一种分治方法,这个类似与序列分治找中点分治,常常用来优化 $dp$ 或 加快合并速度。

例题

LuoguP2634 [国家集训队]聪聪可可

题意

询问树上有多少条路径,使得这条路径长 $\bmod {k = 3}$ 等于 $0$ 。

题解

最裸的一道题。。

其实可以直接 $dp$ ,但如果把 $k=3$ 改成 $k=10^6$ 之类的, $dp$ 就不能做了,只能用点分治。

那样可以达到 $O((n + k) \log n)$ 的优秀的复杂度。

我们考虑每次点分治,然后对于分治重心的每一个子树,统计一下到子树根节点有多少条路径 $\bmod k = b$ 。

然后每次合并的时候,直接枚举其中一个,然后另一个就是 $(k - b) \mod k$ 了。

代码

其实很好写的。

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}

void File() {
#ifdef zjp_shadow
	freopen ("P2634.in", "r", stdin);
	freopen ("P2634.out", "w", stdout);
#endif
}

const int N = 30100, M = N << 1, inf = 0x7f7f7f7f;

int Head[N], Next[M], to[M], val[M], e = 0;
void add_edge(int u, int v, int w) {
	to[++ e] = v; val[e] = w; Next[e] = Head[u]; Head[u] = e;
}
#define Travel(i, u, v) for (register int i = Head[u], v = to[i]; i; v = to[i = Next[i]])

bitset<N> vis;
int sz[N], maxsz[N], nodesum, rt;
void Get_Root(int u, int fa = 0) {
	sz[u] = maxsz[u] = 1;
	Travel(i, u, v)
		if (v != fa && !vis[v]) Get_Root(v, u), sz[u] += sz[v], chkmax(maxsz[u], sz[v]);
	chkmax(maxsz[u], nodesum - sz[u]);
	if (maxsz[u] < maxsz[rt]) rt = u;
}

int tot[3];
void Get_Info(int u, int fa, int dis) {
	++ tot[dis];
	Travel(i, u, v) if (v != fa && !vis[v])
		Get_Info(v, u, (dis + val[i]) % 3);
}

typedef long long ll; ll ans = 0;
int sum[3];
inline void Init() { Set(sum, 0); sum[0] = 1; ++ ans; }
inline void Calc() {
	For (i, 0, 2)
		ans += 2ll * sum[i] * tot[(3 - i) % 3];
	For (i, 0, 2) sum[i] += tot[i];
}

void Solve(int u) {
	vis[u] = true; Init();
	Travel(i, u, v) if (!vis[v])
		Set(tot, 0), Get_Info(v, u, val[i]), Calc();
	Travel(i, u, v) if (!vis[v])
		nodesum = sz[v], rt = 0, Get_Root(v), Solve(rt);
}

int main () {

	File();
	int n = read();
	For (i, 1, n - 1) {
		int u = read(), v = read(), w = read() % 3;
		add_edge(u, v, w); add_edge(v, u, w);
	}
	maxsz[0] = inf; nodesum = n, Get_Root(1), Solve(rt);

	ll gcd = __gcd(ans, 1ll * n * n);
	printf ("%lld/%lld\n", ans / gcd, 1ll * n * n / gcd);

    return 0;
}

点分树

可以发现在点分治结构中,一个点与一个以它为重心的子树对应。如果将当前重心与所有子树的重心相连,得到的树称为 点分树 或者 重心树 。点分树的高度为 $O(\log n)$ ,修改一个点时,将会修改点分树上它到根路径上所有点对应的子树信息。

「ZJOI2015」幻想乡战略游戏

为了充分理解这个数据结构,强烈建议点入我博客中上面对于这道题的题解。

这道题不仅充分展现了点分树的运用,并且我的博客中讲解了带权重心的一个性质,以及求带权距离和的两种方法,作为此处对于点分树的补充。

线段树合并

对两颗线段树(一般为动态开点线段树)合并方法为:

  • 令根节点为 $x, y$
  • 如果其中一颗线段树为空 ( $x = 0$ 或 $y = 0$ ),返回另外一颗
  • 否则递归合并 $x, y$ 的左、右子树,最后合并信息。

假设总共有 $n$ 个信息,那么容易证明最后复杂度是 $O(n \log n)$ 的。这是因为每次合并都会使得一个信息所在的集合大小翻倍。

例题

LOJ #2537. 「PKUWC 2018」Minimax

点进去看看qwq

这个介绍了对于一类状态种数与子树大小有关的 $dp$ 可以考虑用线段树合并统计状态。

LOJ #2359. 「NOIP2016」天天爱跑步

再点进去看看QwQ

这个介绍了一类状态种数与深度大小有关的信息统计可以考虑用线段树合并来统计状态。

以及 NOIP 题,能用高端数据结构来弥补思维缺陷。

其实对于这类状态数与深度有关的合并,有些能用上面介绍的长链剖分来处理,更加优秀。

Codeforces Round #463 F. Escape Through Leaf

还是点进去看看。。。

我们每次考虑一个点,需要对于它子树所有状态进行考虑的时候,可以使用线段树合并。

然后对于这个线段树就能满足子树合并的性质。

一些鬼畜的线段树有可能也可以写出神奇的合并方法。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部