dfs序+树链剖分 入门,总结一下自己所学与所想与做过的一点例题

dfs序,就是按照dfs的顺序遍历一棵树,然后用其路径代表这棵树,化为字符串以便操作。

dfs遍历顺序如下:

路径如下:

跟先序遍历差不多

对于任何一棵子树来说,其序号都是相连的,比如以2为根节点的子树

这样就为我们处理子树问题提供了方便,对于2,我们可以用in数组来记录其进入子树(也就是第一次遍历到2)的时候,然后用out数组来记录其退出子树的时间,那么对于这颗子树,我们就可以用 in[2] ~ out[2]来记录,再具体点可以看下面的例题

dfs遍历时代码如下:

void dfs(int x,int fat) { //x:当前节点  fat: 父亲节点
	in[x] = ++id; //记录in数组位置
	for (int i = head[x]; i != -1; i = nxt[i]) {
		if (i == fat) continue;
		dfs(to[i],x); // 处理其子树
	}
	out[x] = id; //记录out数组位置
}

一般dfs序并不直接解决问题,只是将树一维化使得能和其他的数据结构或者算法结合。

一般其可以解决子树问题

例题1: poj3321

题目:https://vjudge.net/problem/POJ-3321#author=0

题意上面有中文版的

题目操作涉及点修改,子树查询。

将树一维化的时候,因为子树的序号是相连的,所以可以套上一些跟区间有关的算法。而本题需要求区间苹果数量的和,就是区间数量查询问题,另外一个操作则是点修改的问题,那么我们可以使用树状数组。

树状数组的功能包括点修改以及求前k个数组之和,对于区间问题则可以转化为求差来做。

比如有这么一棵树:

其 in  out 数组则为:

id实际上就是我们dfs得到的序号,他们可以表示这棵树,假设我们将这堆数存进id数组里

那么遍历id实际上就是以dfs序遍历这棵树

就拿id=2 这个的in 和 out数组来说

in [2]表示 当id下标为in[2]的时候进入 根节点为2的子树

out[2]表示当id下标为 out[2]的时候子树即将遍历完毕

于是我们就可以愉快的对其进行区间操作了,我们使用树状数组,当我们需要得到根节点为2的子树的所有节点的权值的时候,假设getsum(x) 为 获得下标为1~x的 数组的和

则结果为 getsum(out[2])-getsum(in[2]-1)

代码:

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int maxn = 2e5 + 7;
int C[maxn];
int in[maxn], out[maxn];
int head[maxn], nxt[maxn], to[maxn], no_hav[maxn];
int n, m, cnt, id;
int lowbit(int x) {
	return x & (-x);
}
int getsum(int x) {
	int ans = 0;
	for (int i = x; i > 0; i -= lowbit(i)) ans += C[i];
	return ans;
}
void add(int x, int y) {
	for (int i = x; i <= n; i += lowbit(i)) C[i] += y;
}
void add_edge(int num1, int num2) { //链式前向星
	nxt[++cnt] = head[num1];
	to[cnt] = num2;
	head[num1] = cnt;
}
void dfs(int x) {
	in[x] = ++id;
	for (int i = head[x]; i != -1; i = nxt[i]) {
		dfs(to[i]); //这道题的题目给的是单向边,就不必考虑子节点链父节点了
	}
	out[x] = id;
}
int main() {
	cin >> n;
	cnt = 1;
	memset(head, -1, sizeof(head));
	for (int i = 1; i < n; i++) {
		int num1, num2;
		scanf("%d %d", &num1, &num2);
		add_edge(num1, num2);
	}
	id = 0;
	dfs(1);
	for (int i = 1; i <= n; i++) add(in[i], 1);
	cin >> m;
	while (m--) {
		char inp;
		int x;
		scanf(" %c%d", &inp, &x);
		if (inp == 'Q') {
			printf("%d\n", getsum(out[x]) - getsum(in[x] - 1));
		}
		else {
			if (!no_hav[x]) add(in[x], -1), no_hav[x] = 1;
			else add(in[x], 1), no_hav[x] = 0;
		}
	}
	return 0;
}

 

 

树链剖分

前导知识:线段树,dfs序……

词汇:

重儿子:对于每一个非叶子节点,它的儿子中 儿子数量最多的那一个儿子 为该节点的重儿子

轻儿子:对于每一个非叶子节点,它的儿子中 非重儿子 的剩下所有儿子即为轻儿子

叶子节点没有重儿子也没有轻儿子(因为它没有儿子。。)

重边:连接任意两个重儿子的边叫做重边

轻边:剩下的即为轻边

重链:相邻重边连起来的 连接一条重儿子 的链叫重链

(以上词汇解释来自于https://www.luogu.org/problemnew/solution/P3384 ,建议先从这里入门 ,我的blog只是学完了自己表述一遍我学到了什么而已)

那么对于一棵无辜的树:

则绿色边为重边,由绿色边组成的链是重链

蓝色边为轻边

假如子节点数量一样则随机指定(按遍历顺序)

树链剖分则顾名思义,树链指的是上述的轻链和重链,剖分指的就是一棵树可以拆成若干重轻链,和dfs序一样,本质上也是离散化一棵树,使得可以和一些数据结构结合。

树链剖分解决的是节点到节点的操作,那为什么要剖开呢,我就说说我自己的看法。

首先,假如求树上任意两点路径中的经过的点的权值之和,我们要怎么求呢,当然肯定不可以朴素的一个一个点相加。假如我们能有一种办法使得路径拆一大段一大段区间,然后使用数据结构维护,需要求解的时候直接一大段一大段的加起来,那么就可以减少时间复杂度,树链剖分就给我们提供了一种将路径分段,使用数据结构维护的方法的途径,它有几点需要了解的地方:

1.重边总是优先连续的。

想想我们在dfs序为节点编号的做法,遍历其所有子节点,重边的建立也是如此。

void dfs1(int x, int f, int deep) { //处理深度 重儿子 节点大小
	dep[x] = deep; //deep 深度数组,记录该节点深度
	fat[x] = f; //父亲数组,记录父节点
	siz[x] = 1; //记录以该节点为根节点的子树的大小
	int mx_siz = 0; // 保存最大子树的大小
	for (int i = Head[x]; i != -1; i = Nxt[i]) { //遍历子节点
		if (To[i] == f) continue; //假如遇到父亲节点就跳过
		dfs1(To[i], x, deep + 1); //对子节点dfs
		siz[x] += siz[To[i]];  // 加上该子节点的大小
		if (siz[To[i]] > mx_siz) { //寻找重儿子
			h_son[x] = To[i]; // h_son = heavy son  
			mx_siz = siz[To[i]];
		}
	}
}

所以即使只有一条边,那条边就会是重边。对于每一个结点,除非其是叶子节点,否则必定会有一根重边链接。所以重边优先连接

2.通过重边,我们可以将一棵树竟可能多的串起来,所以我们在对树编号的时候尽量使得在重边上的节点编号连续,这样有利于我们使用线段树区间维护一段节点上的权值。

3.在求树上任意两点中的点的权值之和时,我们可以将这条路径拆分成多段使得这多段路径都可以通过线段树的查询操作得到

这可能也许大概应该就是树链剖分的基本思想。

 

基本的树链剖分做法:

1.根据题目要求将树建立好,一般使用链式前向星建图

void add_edge(int fro, int to) {
	Nxt[++cnt] = Head[fro];
	To[cnt] = to;
	Head[fro] = cnt;
	Nxt[++cnt] = Head[to];
	To[cnt] = fro;
	Head[to] = cnt;
}

2. 对树初次dfs,目的是处理节点深度,记录父亲节点,处理出节点大小进而得到重儿子编号

void dfs1(int x, int f, int deep) { //处理深度 重儿子 节点大小
	dep[x] = deep; //deep 深度数组,记录该节点深度
	fat[x] = f; //父亲数组,记录父节点
	siz[x] = 1; //记录以该节点为根节点的子树的大小
	int mx_siz = 0; // 保存最大子树的大小
	for (int i = Head[x]; i != -1; i = Nxt[i]) { //遍历子节点
		if (To[i] == f) continue; //假如遇到父亲节点就跳过
		dfs1(To[i], x, deep + 1); //对子节点dfs
		siz[x] += siz[To[i]];  // 加上该子节点的大小
		if (siz[To[i]] > mx_siz) { //寻找重儿子
			h_son[x] = To[i]; // h_son = heavy son  
			mx_siz = siz[To[i]];
		}
	}
}

3.通过已经得到的重儿子来为这棵树重新编号(和dfs序类似),注意重链的编号需要优先连续所以对重儿子优先考虑。并且我们需要记录每一个节点的top节点,意思就是对于这个节点其所在的链最上端(靠近根节点)的编号是什么(方便我们查询两点的时候将路径分割多段查询)

void dfs2(int x, int topf) { // 标记和处理新编号,处理每条链及其顶端
	//x : 当前处理到哪个节点  topf: 这个节点的顶端节点是什么
	id[x] = ++cnt; // cnt用于建立新点,新点放在id数组里
	top[x] = topf;
	n_val[cnt] = val[x]; // n_val = new val  即将旧点和新点权值对应
	if (!h_son[x]) return;
	dfs2(h_son[x], topf);//先处理重儿子
	for (int i = Head[x]; i != -1; i = Nxt[i]) {
		if (To[i] == fat[x] || To[i] == h_son[x]) continue;
		dfs2(To[i], To[i]); //轻链的top则是自己
	}
}

4.既然得到新节点编号了,我们就根据这个编号建立线段树(此处略,后面有例题)

5.对于不同的要求码不同的代码。

例题:洛谷p3384 

https://www.luogu.org/problemnew/show/P3384

有4个操作,我就分开简述

操作1: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z

操作2: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和

操作3: 3 x z 表示将以x为根节点的子树内所有节点值都加上z

操作4: 4 x 表示求以x为根节点的子树内所有节点值之和

 

先讲操作2:将x,y节点路径上所有节点的和

看例子,编号我已经标好:

假如查询的是 5~7,那么我们需要找5和7这两点中的top深度最大的,换句话说就是找top最靠近他们的,再换句话说就是找最近的祖先节点。

因为假如我们找top深度浅的,那么就是5这个节点其top是1,就有可能带入不相关的边(1~2)

流程: 找7的top节点-------6节点-----累加他们的答案(线段树)--继续找最近top节点直到相同----2节点-----2节点和5节点祖先节点一样-----累加2节点和5节点的答案

需要注意的就是因为线段树是根据新的编号建立的,和题目输入的编号不一样。

int query_range_xy(int x, int y) {//从x到y节点最短路径上所有节点之和
	int ans = 0;
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]]) swap(x, y);//依然x是更深的那个点
		ans += query(1, 1, n, id[top[x]], id[x]);
		ans %= mod;
		x = fat[top[x]];
	}
	if (dep[x] > dep[y]) swap(x, y); //将y的找了
	///   这里写成 top[x]>top[y]  debug 一下午 
	ans += query(1, 1, n, id[x], id[y]);
	return ans % mod;
}

 

那么操作一实际上就和二类似,只是变成了线段树中的区间更新操作。

 

再说操作4:以x为根节点的子树内所有节点值之和

想一下在dfs序我们为树编号的顺序,再想一下树链剖分编号的顺序。

其实树链剖分只是比dfs序多了一步:优先为重儿子编号,其他都没变,因此对于一颗子树的节点编号依然还是连在一起的。

假如根节点编号是x,我们没有设置in,out数组,但我们有size(子树大小)数组啊!我们只需要查询 x 到 x+size-1就可以了。

int query_root_x(int x) { //以x为根节点的子树内所有节点值之和
	return query(1, 1, n, id[x], id[x] + siz[x] - 1) % mod;
}

 

好了放完整代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 7;
int Head[maxn], Val[maxn], To[maxn], Nxt[maxn];//链式前向星数组大写开头
int dep[maxn], fat[maxn], siz[maxn], h_son[maxn], id[maxn], top[maxn];
//深度  父节点   大小    重儿子编号   节点新编号    链的顶端节点
int n_val[maxn], val[maxn];
// 新编号的val   旧编号的val
int tree[maxn], lazy[maxn];
int n, m, root, mod, cnt;
void init() {
	memset(h_son, 0, sizeof(h_son));
	memset(Head, -1, sizeof(Head));
	cnt = 1;
}
void add_edge(int fro, int to) {
	Nxt[++cnt] = Head[fro];
	To[cnt] = to;
	Head[fro] = cnt;
	Nxt[++cnt] = Head[to];
	To[cnt] = fro;
	Head[to] = cnt;
}
void dfs1(int x, int f, int deep) { //处理深度 重儿子 节点大小
	dep[x] = deep; //deep 深度数组,记录该节点深度
	fat[x] = f; //父亲数组,记录父节点
	siz[x] = 1; //记录以该节点为根节点的子树的大小
	int mx_siz = 0; // 保存最大子树的大小
	for (int i = Head[x]; i != -1; i = Nxt[i]) { //遍历子节点
		if (To[i] == f) continue; //假如遇到父亲节点就跳过
		dfs1(To[i], x, deep + 1); //对子节点dfs
		siz[x] += siz[To[i]];  // 加上该子节点的大小
		if (siz[To[i]] > mx_siz) { //寻找重儿子
			h_son[x] = To[i]; // h_son = heavy son  
			mx_siz = siz[To[i]];
		}
	}
}
void dfs2(int x, int topf) { // 标记和处理新编号,处理每条链及其顶端
	//x : 当前处理到哪个节点  topf: 这个节点的顶端节点是什么
	id[x] = ++cnt; // cnt用于建立新点,新点放在id数组里
	top[x] = topf;
	n_val[cnt] = val[x]; // n_val = new val  即将旧点和新点权值对应
	if (!h_son[x]) return;
	dfs2(h_son[x], topf);//先处理重儿子
	for (int i = Head[x]; i != -1; i = Nxt[i]) {
		if (To[i] == fat[x] || To[i] == h_son[x]) continue;
		dfs2(To[i], To[i]); //轻链的top则是自己
	}
}

// 对新节点建立线段树
void build_tree(int l, int r, int ind) {
	if (l == r) {
		tree[ind] = n_val[l] % mod; //
		return;
	}
	int mid = (l + r) >> 1;
	build_tree(l, mid, ind << 1);
	build_tree(mid + 1, r, ind << 1 | 1);
	tree[ind] = (tree[ind << 1] + tree[ind << 1 | 1]) % mod;
}
void push_down(int ind, int len) {
	lazy[ind << 1] += lazy[ind];
	lazy[ind << 1 | 1] += lazy[ind];
	tree[ind << 1] += lazy[ind] * (len - (len >> 1));
	tree[ind << 1] %= mod;
	tree[ind << 1 | 1] += lazy[ind] * (len >> 1);
	tree[ind << 1 | 1] %= mod;
	lazy[ind] = 0;
}
int query(int ind, int l, int r, int q_l, int q_r) {
	if (q_l <= l && q_r >= r) {
		return tree[ind] % mod;//
	}
	int res = 0;
	int mid = (l + r) >> 1;
	if (lazy[ind]) push_down(ind, r - l + 1);
	if (q_l <= mid) res += query(ind << 1, l, mid, q_l, q_r);
	if (q_r > mid) res = (res + query(ind << 1 | 1, mid + 1, r, q_l, q_r)) % mod;
	return res % mod; //
}
void update(int ind, int l, int r, int q_l, int q_r, int val) {
	if (q_l <= l && q_r >= r) {
		lazy[ind] += val;
		tree[ind] += (val * (r - l + 1)) % mod;
		tree[ind] %= mod;
		return;
	}
	if (lazy[ind]) push_down(ind, r - l + 1);
	int mid = (l + r) >> 1;
	if (q_l <= mid) update(ind << 1, l, mid, q_l, q_r, val);
	if (q_r > mid) update(ind << 1 | 1, mid + 1, r, q_l, q_r, val);
	tree[ind] = (tree[ind << 1] + tree[ind << 1 | 1]) % mod;
}

//注意,线段树的编号已经不是原编号,因此需要转化
void add_range_xy(int x, int y, int val) {//x~y节点的最短路径上所有节点的值+val
	val %= mod;
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]]) swap(x, y);// x是深度大的
		update(1, 1, n, id[top[x]], id[x], val); //更新深度大的那一条链
		x = fat[top[x]];
	}
	if (dep[x] > dep[y]) swap(x, y);
	update(1, 1, n, id[x], id[y], val);
}
int query_range_xy(int x, int y) {//从x到y节点最短路径上所有节点之和
	int ans = 0;
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]]) swap(x, y);//依然x是更深的那个点
		ans += query(1, 1, n, id[top[x]], id[x]);
		ans %= mod;
		x = fat[top[x]];
	}
	if (dep[x] > dep[y]) swap(x, y); //将y的找了
	///   这里写成 top[x]>top[y]  debug 一下午 
	ans += query(1, 1, n, id[x], id[y]);
	return ans % mod;
}
void add_root_x(int x, int val) { //以x为根节点的子树内所有节点的值都加上val
	//printf("val= %d\n", id[x]+siz[x]);
	update(1, 1, n, id[x], id[x] + siz[x] - 1, val);
}
int query_root_x(int x) { //以x为根节点的子树内所有节点值之和
	return query(1, 1, n, id[x], id[x] + siz[x] - 1) % mod;
}
int main() {
	init();
	cin >> n >> m >> root >> mod;
	for (int i = 1; i <= n; i++) {
		scanf("%d", val + i);
		val[i] %= mod;
	}
	for (int i = 1; i < n; i++) {
		int num1, num2;
		scanf("%d %d", &num1, &num2);
		add_edge(num1, num2);
	}
	dfs1(root, 0, 1);// 0 节点需要没出现过
	cnt = 0; // 此时cnt为新编号的id
	dfs2(root, root);
	build_tree(1, n, 1);
	int flag;
	while (m--) {
		scanf("%d", &flag);
		int inp1, inp2, inp3;
		switch (flag) {
		case 1: {
			scanf("%d %d %d", &inp1, &inp2, &inp3);
			add_range_xy(inp1, inp2, inp3);
			break;
		}
		case 2: {
			scanf("%d %d", &inp1, &inp2);
			printf("%d\n", query_range_xy(inp1, inp2));
			break;
		}
		case 3: {
			scanf("%d %d", &inp1, &inp2);
			add_root_x(inp1, inp2);
			break;
		}
		default: {
			scanf("%d", &inp1);
			printf("%d\n", query_root_x(inp1));
		}
		}
	}
	return 0;
}

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值