「学习笔记」树链剖分

一.树链剖分的概念

树链剖分是一种对树进行划分的算法,将树划分为若干个链,以便维护树上路径的信息。

也就是说,我们将树划分为若干个线性结构,使用一些数据结构来维护它,如:树状数组,线段树,splay等。


二.树链剖分的作用

前面我们提到,我们将树划分为多个线性结构,用数据结构来维护它,所以我们可以很快捷地对于树上的某些信息进行修改或者查询等操作,例如:修改某条树上路径的所有点权;查询某条树上路径点权的极值、和、等。


三.重链剖分

我们先要了解一些很重要的定义。

重子节点:其子节点中子树大小最大的节点,若有多个,任取其一。若没有子节点,那么久没有重子节点。

轻子节点:其子节点中除重子节点以外的所有子节点。

重边:从当前节点到重子节点的边。

轻边:从当前节点到轻子节点的边。

重链:若干条相连接的重边

这里给出一张摘自oi-wiki的图片供读者理解。


四.实现

树链剖分的实现分为 \(2\)\(dfs\)

第一个\(dfs\)记录每个节点的父节点(\(fa\)),深度(\(dep\)),子树大小(\(siz\)),重子节点(\(son\))。

第二个\(dfs\)记录每个节点所在链的链顶(\(top\)),dfs序(\(dfn\)),\(dfs\) 序对应的节点编号 (\(sgt\))。

代码实现如下:

void dfs1 (int u, int faa, int depp) {
	fa[u] = faa;
	siz[u] = 1;
	dep[u] = depp;
	
	for (int i = head[u]; i; i = e[i].nxt) {
		int v = e[i].to;
		
		if (v == faa) {
			continue;
		}
		
		dfs1 (v, u, depp + 1);
		
		siz[u] += siz[v];
		
		if (siz[v] > siz[son[u]]) {
			son[u] = v;
			//找重子节点。
		}
	}
}

void dfs2 (int x, int topp) {
	top[x] = topp;
	dfn[x] = ++tot;
	sgt[tot] = ipt[x];
	//ipt是输入的点权。
	
	if (!son[x]) {
		return ;
	}
	
	dfs2 (son[x], topp);
	//优先对重子节点进行遍历,可以保证一条链上的节点的dfs序连续。

	for (int i = head[x]; i; i = e[i].nxt) {
		int v = e[i].to;
		
		if (v != fa[x] && v != son[x]) {
			dfs2 (v, v);
            //当一个节点位于轻链底端时,它的top就是它本身。
		}
	}
}

五.应用

我们先给出一些变量名的定义:

top[i]//表示节点i所在的重链的顶部节点。
dfn[i]//表示节点i的dfs序,也是节点i在线段树中的序号。
dep[i]//表示节点i的深度。
fa[i]//表示节点i的父亲。
siz[i]//表示节点i的子树的节点个数。
son[i]//表示节点i的重子节点。
sgt[i]//表示dfs序所对应的节点编号。

求两个节点 \(x\)\(y\) 的路径上的点权之和:

int query_tree (int x, int y) {
	int ans = 0;
	
	while (top[x] != top[y]) {
   	//x和y不在同一条重链上。
		if (dep[top[x]] < dep[top[y]]) {
			swap (x, y);
		} 
		
		ans += query (1, dfn[top[x]], dfn[x]);
        //用线段树维护链上信息。
		
		x = fa[top[x]];
        //将x设为原重链链头的父节点,继续从轻边循环。
	}
	
	if (dep[x] > dep[y]) {
		swap (x, y);
	}
    //x和y已经在同一条重链上了。
	
	ans += query (1, dfn[x], dfn[y]);
    //用线段树维护链上信息。
	
	return ans;
}

这个操作就很像 \(LCA\) ,我们使用了 \(top\) 进行加速。

注意,我们每次循环的时候只能让同一个节点向上跳,否则会出现少计算答案的情况。

同理,修改树上某条路径的点权也是一样的。


求以节点 \(x\) 为根节点的子树内所有点权和:

我们知道,我们将树划分为了多个线性结构,也就是链。

于是,我们就可以利用一些数据结构进行维护。

所以,我们在查询子树和的时候,只需要查询一段区间的和。

这里我用线段树进行查询,代码如下:

query (1, dfn[x], dfn[x] + siz[x] - 1);

六.例题

例题 \(1\)

P3384 【模板】轻重链剖分/树链剖分

这道题就是树链剖分的模板题。

对于 \(3\)\(4\) 操作,我们可以将其化为线性结构进行维护。

\(1\)\(2\) 操作,我们只需要进行树上操作即可。

操作 \(1\) :

void addtree (int x, int y, int k) {
	while (top[x] != top[y]) {
	//不在同一条重链上。
		if (dep[top[x]] < dep[top[y]]) {
			swap (x, y);
		}
		
		addS (1, dfn[top[x]], dfn[x], k);
        //线段树区间修改。
		
		x = fa[top[x]];
        //向上跳转。
	}
	
	if (dep[x] > dep[y]) {
		swap (x, y);
	}
    //现在节点x和y在同一条重链上。
	
	addS (1, dfn[x], dfn[y], k);
    //线段树区间修改。
}

操作 \(2\)

int query_tree (int x, int y) {
	int ans = 0;
	
	while (top[x] != top[y]) {
	//不在同一条重链上。
		if (dep[top[x]] < dep[top[y]]) {
			swap (x, y);
		} 
		
		ans += query (1, dfn[top[x]], dfn[x]);
        //线段树区间查询。            
		ans %= p;
		
		x = fa[top[x]];
        //向上跳转。
	}
	
	if (dep[x] > dep[y]) {
		swap (x, y);
	}
    
    //节点x和y在同一条重链上的时候。
	
	ans += query (1, dfn[x], dfn[y]);
	//线段树区间查询。
    
	return ans %p;
}

操作 \(3\)

addS (1, dfn[x], dfn[x] + siz[x] - 1, y % p);
//运用线段树的区间修改。

操作 \(4\)

printf ("%d\n", query (1, dfn[x], dfn[x] + siz[x] - 1) %p);
//运用线段树的区间查询。

完整代码。


例题 \(2\)

P3178 [HAOI2015]树上操作

这里仅仅给出了 \(3\) 个操作让选手们实现。

对于操作 \(1\) :我们可以用线段树的区间修改实现单点修改。

对于操作 \(2\) :我们可以用线段树的区间修改实现树上修改。

对于操作 \(3\) :我们用树上操作进行维护。

操作 \(1\) 代码:

add (1, dfn[x], dfn[x], y);
//用线段树进行维护。

操作 \(2\) 代码:

add (1, dfn[x], dfn[x] + siz[x] - 1, y);
//运用线段树实现树上修改。

操作 \(3\) 代码:

ll querytree (int x, int y) {
	int ans = 0;
	
	while (top[x] != top[y]) {
	//不在同一条重链上。
		if (dep[top[x]] < dep[top[y]]) {
			swap (x, y);
		} 
		
		ans += query (1, dfn[top[x]], dfn[x]);
        //线段树区间查询。            

		x = fa[top[x]];
        //向上跳转。
	}
	
	if (dep[x] > dep[y]) {
		swap (x, y);
	}
    
    //节点x和y在同一条重链上的时候。
	
	ans += query (1, dfn[x], dfn[y]);
	//线段树区间查询。
    
	return ans;
}

完整代码。


例题 \(3\)

P2590 [ZJOI2008]树的统计

题目给出了 \(3\) 个操作让选手实现。

操作 \(1\):修改单点点权。

操作 \(2\):查询树上路径最大点权。

操作 \(3\):查询树上路径点权之和。

对于第 \(1\) 个操作,我们直接使用线段树的单点修改操作即可。

对于第 \(2\) 个操作,我们仿照上文的树上操作,采用线段树维护区间最大值,在树上进行操作。

对于第 \(3\) 个操作,我们已经在前文反复提到过了。

这里给出第 \(2\) 个操作的代码:

int querytreemax (int x, int y) {
	int ans = -inf;
	
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]]) {
			swap (x, y);
		}
		
		ans = max (ans, querymax (1, dfn[top[x]], dfn[x]));
		//线段树查询区间最大值。
		x = fa[top[x]];
	}
	
	if (dep[x] > dep[y]) {
		swap (x, y);
	}
    这里的x和y在同一重链上了。
	
	ans = max (ans, querymax (1, dfn[x], dfn[y]));
    //线段树查询区间最大值。
	
	return ans;
}

完整代码。


例题 \(4\)

P2146 [NOI2015] 软件包管理器

很显然的树剖。

对于第一种操作,可以认为将 \(x\) 和与 \(x\) 有依赖的所有软件包都赋为 \(1\),也就是说将 \(0-x\) 都变为 \(1\)

对于第二种操作,也就是说将 \(x\) 的子树所有节点赋为 \(0\)

输出时要记录操作前的值,相减即可。

完整代码。

posted @ 2021-07-26 11:51  cyhyyds  阅读(134)  评论(0编辑  收藏  举报