哇!树链剖分(树链剖分详解)


听说有人不会树链剖分?

前置芝士

  • 线段树
  • 树状数组
  • SBT
  • 平衡树

以上四种任意一种即可,这里主要讲线段树做法。

引入

树链剖分(Tree Line Pow Divide),一种解决树上快速路径修改查询问题的算法,一般指 重链剖分(Heavy Path Decomposition)。

思想图解

一个问题

如题,已知一棵包含 N N N 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:

  • 1 x y z,表示将树从 x x x y y y 结点最短路径上所有节点的值都加上 z z z

  • 2 x y,表示求树从 x x x y y y 结点最短路径上所有节点的值之和。

一些定义

顾名思义,重链剖分就是把”重“的链与其他的链分开,那么如何定义重呢?

我们定义一个 重儿子(Heavy Son)的概念:

以该点的儿子为根的子树中节点个数最多的儿子为重儿子

那么就可以递归定义 重链

若节点 u 为节点 v 的重儿子,那么 v 就归入到 u 所在的链中

否则,节点 v 就单独成为一条链的链首

那么一棵树可以被剖成这个样子:

一棵树

其中一个长方形代表一条链。

接下来即可定义一个 链顶的概念:

一条链中,深度最低的点

深度,即

根节点到该节点的距离+1

思想概述

看到类似区修区查的语言:

  • 所有节点的值都加上 z z z

  • 所有节点的值之和。

不难想到用线段树(或树状数组等,下同);

但是难想的就是如何将一棵树剖成一个序列,从而使用线段树呢?

我们可以将树中的的节点重新编号,按照编号顺序建线段树,

其中编号序列满足以下条件(先不说为什么,待会再讲):

所有的重链的编号是连续的

是一种dfs序

这里我不想画图了,读者自己体会。

可以建树了,但是修改查询还不会。

修改

我们先考虑一种简单的情况:

情况A

修改的两个点 A,B 在同一条重链上:

一种情况

根据我们dfs序的建立,易证 A 到 B 的路径上的节点在线段树上一定是连续的。

那么就可以通过线段树的区间修改操作实现了。
Change ( i d A , i d B , v a l ) \texttt{Change}(id_A,id_B,val) Change(idA,idB,val)
这里就可以填上我刚才挖的那个坑了。

情况B
再引入一下

一个很好理解的定理(废话):

任意一个链顶不为根的链,链顶的父亲一定是另外一条链的一部分

同样,这里我不想画图了,读者自己体会。

接下来要讨论的情况就是不在同一条链上:

另一种情况

我们可以先把链顶深度较低的 B 所在的这条链的所有点修改了,并跳到其链顶的父亲所在的链的最后一个节点上;
Change ( i d t o p B , i d B , v a l ) B = f a t o p B \texttt{Change}(id_{top_B},id_B,val)\\ B=fa_{top_B} Change(idtopB,idB,val)B=fatopB

接着同理修改 A;
Change ( i d t o p A , i d A , v a l ) A = f a t o p A \texttt{Change}(id_{top_A},id_A,val)\\ A=fa_{top_A} Change(idtopA,idA,val)A=fatopA

即每次将链顶深度较低的往上爬,直到 A 与 B 重合。

其实,可以将两者结合一下:

每次将链顶深度较低的往上爬,直到 A 与 B 在同一条链。

施行情况A(因为 A 与 B 在同一条链上并不意味着 A = B)

查询

与修改大同小异,只是把 Change \texttt{Change} Change 换成 Query \texttt{Query} Query 而已,这里不赘述。

代码

模板题 洛谷 P3384 【模板】重链剖分/树链剖分

int a[Maxn], tmp[Maxn];
int p;

struct SegmentTree {//线段树
#define ls (id << 1)
#define rs (id << 1 | 1)
	struct Segment {
		int Left;
		int Right;
		int valMax;
		int tag;
		int valSum;
	} seg[Maxn << 2];
	il void PushUp(int id) {
		seg[id].valMax = max(seg[ls].valMax, seg[rs].valMax) % p;
		seg[id].valSum = (seg[ls].valSum + seg[rs].valSum) % p;
		return;
	}
	il void PushDown(int id) {
		if (seg[id].tag) {
			seg[ls].tag += seg[id].tag;
			seg[ls].tag %= p;
			seg[ls].valSum += seg[id].tag * (seg[ls].Right - seg[ls].Left + 1);
			seg[ls].valSum %= p;
			seg[rs].tag += seg[id].tag;
			seg[rs].tag %= p;
			seg[rs].valSum += seg[id].tag * (seg[rs].Right - seg[rs].Left + 1);
			seg[rs].valSum %= p;
			seg[id].tag = 0;
		}
		return;
	}
	il void Build(int id, int Left, int Right) {
		seg[id] = {Left, Right, 0, 0, 0};
		if (Left == Right) {
			seg[id].valMax = a[Left] % p;
			seg[id].valSum = a[Left] % p;
			return;
		}
		int mid = (Left + Right) >> 1;
		Build(ls, Left, mid);
		Build(rs, mid + 1, Right);
		PushUp(id);
		return;
	}
	il int QuerySum(int id, int Left, int Right) {
		PushDown(id);
		if (seg[id].Right < Left || seg[id].Left > Right) {
			return 0;
		}
		if (Left <= seg[id].Left && seg[id].Right <= Right) {
			return seg[id].valSum % p;
		}
		return (QuerySum(ls, Left, Right) + QuerySum(rs, Left, Right)) % p;
	}
	il void Change(int id, int Left, int Right, int val) {
		PushDown(id);
		if (seg[id].Right < Left || seg[id].Left > Right) {
			return;
		}
		if (seg[id].Left >= Left && Right >= seg[id].Right) {
			seg[id].tag += val;
			seg[id].tag %= p;
			seg[id].valSum += val * (seg[id].Right - seg[id].Left + 1) % p;
			seg[id].valSum %= p;
			return;
		}
		Change(ls, Left, Right, val);
		Change(rs, Left, Right, val);
		PushUp(id);
		return;
	}
};//以上内容不做解释

vector<int> G[Maxn];//邻接表
int n, m, root;

struct Qtree {//重链剖分
	struct treeNode {
		int fa;//该节点的父亲
		int son;//重儿子
		int dep;//深度
		int size;//字数节点个数
		int top;//该点所在链的链顶
		int tid;//重新编号的序号
	} tn[Maxn];
	SegmentTree SEG;
	int tot = 0;
	void dfs1(int step, int fa) {//初始化fa、dep、size、son
		tn[step].fa = fa;
		tn[step].dep = tn[fa].dep + 1;
		tn[step].size = 1;
		int Max = 0;
		for (auto x : G[step]) {
			if (x == fa) {
				continue;
			}
			dfs1(x, step);
			tn[step].size += tn[x].size;
			if (tn[x].size > Max) {//判重儿子
				Max = tn[x].size;
				tn[step].son = x;
			}
		}
		return;
	}
	void dfs2(int step, int top) {//初始化top、tid
		tn[step].top = top;
		tn[step].tid = ++tot;//有没有像Tarjan的dfn?
		a[tot] = tmp[step];//重新将点权赋值
		if (tn[step].son)//避免死循环
			dfs2(tn[step].son, top);//重儿子
		for (auto x : G[step]) {
			if (x == tn[step].fa || x == tn[step].son) {//排除重儿子
				continue;
			}
			dfs2(x, x);//因为x不是重儿子,所以x所在链的链首为自己
		}
	}
	void Build() {//建立
		dfs1(root, 0);//以root为根dfs
		dfs2(root, root);
		SEG.Build(1, 1, n);//以a数组建立线段树
		return;
	}
	void Change(int u, int v, int w) {
		while (tn[u].top != tn[v].top) {//u,v不在同一条链上
			if (tn[tn[u].top].dep < tn[tn[v].top].dep) {//简洁写法,即把链顶深度较低的点放到u
				swap(u, v);
			}
			SEG.Change(1, tn[tn[u].top].tid, tn[u].tid, w % p);//修改
			u = tn[tn[u].top].fa;//往上爬
		}
		if (tn[u].tid > tn[v].tid) {//最后执行情况A
			swap(u, v);
		}
		SEG.Change(1, tn[u].tid, tn[v].tid, w % p);
		return;
	}
	int QuerySum(int u, int v) {//同理
		int Max = 0;
		while (tn[u].top != tn[v].top) {
			if (tn[tn[u].top].dep < tn[tn[v].top].dep) {
				swap(u, v);
			}
			Max += SEG.QuerySum(1, tn[tn[u].top].tid, tn[u].tid);
			Max %= p;
			u = tn[tn[u].top].fa;
		}
		if (tn[u].tid > tn[v].tid) {
			swap(u, v);
		}
		return Max + SEG.QuerySum(1, tn[u].tid, tn[v].tid);
	}
} Qt;

最后读者可以自行思考一下如何将点权转成边权,洛谷 P4114 Qtree1。

THE END

  • 32
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值