【学习笔记】(15) Kruskal 重构树

前置知识:kruskal 求最小生成树,倍增。

1. 算法简介

以下所有讨论基于 最小生成树。

在 Kruskal 求最小生成树的过程:将所有边按照边权排序,若当前边 \((u,v)\) 两端不连通,则往生成树边集 \(E\) 中加入 \((u,v)\) 并连接 \(u,v\)。使用并查集维护连通性。

如果能用某种结构描述每条边被连接的先后顺序,因为越往后加入的边权越大,就可以快速刻画边权有限制时整张图的连通情况。

Kruskal 重构树诞生了。它在 kruskal 基础上进行了一些改进:连接 \(u,v\) 时,找到 \(u,v\) 的代表元 \(U,V\) (相当于 并查集中的 \(fa_u, fa_v\))。新建节点 \(c\),将并查集中 \(U,V\) 的父亲设为 \(c\),并在 \(T\) 上连边 \(c→U\)\(c→V\)。注意 \(U,V\) 可能不是原树节点。

通常设 \(c\) 的权值 \(w_c\)\(w_{u,v}\) 。为虚点设置权值方便解题,巧妙设置点权对解题有极大帮助。

若原图 \(G\) 连通,则得到一棵大小为 \(2n−1\) 且以 \(2n−1\) 为根的 有根树 \(T\)。它就是 Kruskal 重构树,其性质在下一节中介绍。

void merge(int u, int v, int w) {
  	if((u = find(u)) == (v = find(v))) return;
  	node++, fa[u] = fa[v] = fa[node] = node, val[node] = w;
  	add(node, u), add(node, v);
}

这是原图 \(G\)

这是他的 Kruskal 重构树

2.性质与应用

下文称 原节点 为原图节点,共 \(n\) 个,表示原图某个点;新节点 为新建节点,共有 \(n−1\)个,表示原图某条边。

Kruskal 重构树 \(T\) 有很多优秀性质:

  1. \(T\) 是一棵二叉树,由构建方法可知。对于部分题目,特殊重构树建法可有效减小常数,详见 Part 1.3 点权多叉重构树。
  2. \(G\) 的所有节点是 \(T\) 的 叶子。因此原节点和重构树叶子节点本质相同。
  3. 对于任意新节点 \(u\) 及其祖先 \(v,w_u≤w_v\)

上述性质均可以由上图直观得知。

性质 3 非常重要,它是 Kruskal 重构树的核心:原节点 \(x\) 在原图上经过权值 \(≤d\) 的边可达的所有点就是它在 \(T\) 上最浅的权值 \(≤d\) 的祖先 \(a\) 的子树内所有叶子节点。一般倍增求解 \(a\)

相当于从原节点 \(x\) 倍增找到权值 \(≤d\) 的最浅祖先 \(a\),那么 \(a\) 子树内所有叶子就是原图仅保留边权 \(≤d\) 的边时 \(x\) 所在连通块的所有点。

综上,我们可以总结出一个常用套路:当题目限制形如 “只经过权值不大于某个值的点或边” 时,从 Kruskal 重构树角度入手。部分题目也可以使用可持久化并查集,因为它同样能够刻画存在边权限制时图的连通情况:实现 Kruskal 最小生成树的过程中将并查集可持久化。相较于 Kruskal 重构树,可持久化并查集在时空复杂度上更劣,不建议使用。

3.点权多叉重构树

Kruskal 重构树不仅适用于限制边权的题目,也可以处理限制点权的情况。

方法一:

为原图每条边巧妙赋值,将点权转化为边权。若限制经过的点权最大值,因为走一条边 (u,v)
需满足 \(w_u,w_v\) 都不超过限制,所以 \(w_{u,v}=max(w_v,w_v)\)。类似地,若限制最小值则 \(w_{u,v}=min(w_u,w_v)\)

方法二:

实际上我们几乎用不到 \(T\) 是二叉树这一性质,因此存在更高妙的做法。

不妨设题目限制点权最大值。将节点按权值从小到大排序,按序遍历每个点 \(i\)
及其所有出边 \((i,u)\)。若 \(u\) 已经遍历过,则 \(w_i≥w_u\)\(max(w_i,w_u)\) 取到 \(w_i\),此时若 \(i,u\) 不连通则从 \(i 向 u\) 的代表元连边。

上述做法与一般 Kruskal 重构树几乎等价:普通重构树中点权相同的虚点 仅有深度最小的有用;按权值从小到大枚举节点相当于对所有边排序,因为边权即 \(max(w_i,w_u)\)。这样做不用新建虚点,有效减小了常数。

推荐遇到点权重构树时尽量使用方法二,建出多叉重构树,其写法见例题 I。但读者应时刻记住 可以保证重构树是二叉树。

例题

Ⅰ. P4899 [IOI2018] werewolf 狼人

是否存在一条 \((s_i, t_i)\) 的路径,满足先只走编号超过 $ L_i$ ​的点,再走编号不超过 \(R_i\) 的点。

由于给定了路径上点权上下界,很容易想 Kruskal 重构树。

建立两棵重构树。

\(L\) : 按照点权从小到大的重构树。

\(R\) : 按照点权从大到小的重构树。

\(R\) 上从 \(S\) 倍增到使 \(w_a\) 最小且 \(w_a \ge L\) 的祖先 \(a\), 在 \(L\) 上从 \(T\) 倍增到使 \(w_b\) 最大且 \(w_b \le R\) 的祖先 \(b\)。判断 \(a\) 的子树与 \(b\) 的子树有没有交集即可。

可以使用主席树,我这里用的是 BIT。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 67;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, m, q, lg;
struct linklist{
	int tot, Head[N], Next[N << 2], to[N << 2];
	linklist() {tot = 0, memset(Head, 0, sizeof(Head));}
	void add(int u, int v){
		to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
	}
}E, ins, del;
struct Tree{
	int fa[N];
	int find(int x){return fa[x] == x ? x : fa[x] = find(fa[x]);}
	void init(){for(int i = 1; i <= n; ++i) fa[i] = i;}
	linklist G;
	int f[N][20], sz[N], dfn[N], cnt;
	void merge(int u, int v){
		if((v = find(v)) == u) return ;
		fa[v] = f[v][0] = u, G.add(u, v);
	} 
	void dfs(int x){
		dfn[x] = ++cnt, sz[x] = 1;
		for(int i = G.Head[x]; i; i = G.Next[i]) dfs(G.to[i]), sz[x] += sz[G.to[i]];
	}
	void Build(int x){
		dfs(x);
		for(int i = 1; i <= lg; ++i)
			for(int j = 1; j <= n; ++j)
				f[j][i] = f[f[j][i - 1]][i - 1];
	}
	int anc(int u, int lim, bool t){ //寻找满足限制的最浅的祖先 
		for(int i = lg; ~i; --i)
			if(f[u][i] && (t ? f[u][i] >= lim : f[u][i] <= lim))
			 	u = f[u][i];
		return u;
	}
}L, R;
struct BIT{
	int c[N];
	void add(int x){for(; x <= n; x += x & -x) ++c[x];}
	int ask(int x){
		int res = 0;
		for(; x; x -= x & -x) res += c[x];
		return res;
	}
	int query(int l, int r){return ask(r) - ask(l - 1);}
}tr;
int l[N], r[N], ans[N], id[N];
int main(){
	n = read(), m = read(), q = read(), lg = log2(n);
	for(int i = 1; i <= m; ++i){
		int u = read() + 1, v = read() + 1;
		E.add(u, v), E.add(v, u);
	}
	L.init(), R.init();
	for(int i = 1; i <= n; ++i)
		for(int j = E.Head[i]; j; j = E.Next[j])
			if(i > E.to[j])
				L.merge(i, E.to[j]);
	for(int i = n; i; --i)
		for(int j = E.Head[i]; j; j = E.Next[j])
			if(i < E.to[j])
				R.merge(i, E.to[j]);
	L.Build(n), R.Build(1);
	for(int i = 1; i <= n; ++i) id[L.dfn[i]] = R.dfn[i];
	for(int i = 1; i <= q; ++i){
		int s = read() + 1, e = read() + 1, ql = read() + 1, qr = read() + 1;
		s = R.anc(s, ql, 1);
		e = L.anc(e, qr, 0);
		del.add(L.dfn[e] - 1, i); // 不在范围内的点为 [1, L.dfn[e] - 1] 
		ins.add(L.dfn[e] + L.sz[e] - 1, i);
		l[i] = R.dfn[s], r[i] = R.dfn[s] + R.sz[s] - 1;
	}
	for(int i = 1; i <= n; ++i){ //判断有没有交集 
		tr.add(id[i]);
		for(int j = ins.Head[i]; j; j = ins.Next[j]){
			int y = ins.to[j];
			ans[y] += tr.query(l[y], r[y]);
		}
		for(int j = del.Head[i]; j; j = del.Next[j]){
			int y = del.to[j];
			ans[y] -= tr.query(l[y], r[y]); //减去不在范围内的点 
		}
	}
	for(int i = 1; i <= q; ++i) printf("%d\n", (ans[i] > 0));
 	return 0;
}

Ⅱ. P7834 [ONTAK2010] Peaks 加强版

看到只经过权值 \(\le x\) 的边可以想到建立 Kruskal 重构树。
静态区间第 \(k\) 大可以想到建立主席树。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 67;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, m, q, lg, ans;
int tot, Head[N], Next[N << 1], to[N << 1];
void add(int u, int v){
	to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
}
struct Edge{
	int u, v, w;
	bool operator < (const Edge &I) const{return w < I.w;}
}A[N];
struct Tree{
	int fa[N], val[N], id[N], f[N][20], sz[N], dfn[N], st[N], ed[N], cnt, node;
	int find(int x){return fa[x] == x ? x : fa[x] = find(fa[x]);}
	void init(){for(int i = 1; i <= n; ++i) fa[i] = i; node = n;}
	void merge(int u, int v, int w){
		if((v = find(v)) == (u = find(u))) return ;
		++node, fa[u] = fa[v] = fa[node] = node, val[node] = w;
		add(node, u), add(node, v);
	} 
	void dfs(int x){
		dfn[x] = ++cnt, id[cnt] = x, st[x] = cnt;
		for(int i = Head[x]; i; i = Next[i]) dfs(to[i]), sz[x] += sz[to[i]], f[to[i]][0] = x;
		if(!sz[x]) sz[x] = 1; ed[x] = cnt;
	}
	void Build(int x){
		dfs(x);
		for(int i = 1; i <= lg; ++i)
			for(int j = 1; j <= node; ++j)
				f[j][i] = f[f[j][i - 1]][i - 1];
	}
	int anc(int u, int lim){  
		for(int i = lg; ~i; --i)
			if(f[u][i] && val[f[u][i]] <= lim)
			 	u = f[u][i];
		return u;
	}
}T;
struct SegmentTree{
	int lc[N << 5], rc[N << 5], val[N << 5], cnt;
	int modify(int v, int l, int r, int w){
		int u = ++cnt;
		lc[u] = lc[v], rc[u] = rc[v], val[u] = val[v] + 1;
		if(l == r) return u;
		int mid = (l + r) >> 1;
		if(w <= mid) lc[u] = modify(lc[v], l, mid, w);
		else rc[u] = modify(rc[v], mid + 1, r, w);
		return u;
	}
	int query(int u, int v, int l, int r, int k){
		if(l == r) return l;
		int x = val[rc[u]] - val[rc[v]];
		int mid = (l + r) >> 1;
		if(k > x) return query(lc[u], lc[v], l, mid, k - x);
		else return query(rc[u], rc[v], mid + 1, r, k); 
	}
}tr;
int b[N], a[N], rt[N];
int main(){
	n = read(), m = read(), q = read(), lg = log2(n) + 1;
	for(int i = 1; i <= n; ++i) b[i] = a[i] = read();
	sort(b + 1, b + 1 + n);
	int nn = unique(b + 1, b + 1 + n) - b - 1;
	for(int i = 1; i <= n; ++i) a[i] = lower_bound(b + 1, b + 1 + nn, a[i]) - b; 
	for(int i = 1; i <= m; ++i){
		int u = read(), v = read(), w = read();
		A[i] = (Edge){u, v, w};
	}
	sort(A + 1, A + 1 + m);
	T.init();
	for(int i = 1; i <= m; ++i) T.merge(A[i].u, A[i].v, A[i].w);
	T.Build(T.node); 
	for(int i = 1; i <= T.node; ++i){
		rt[i] = rt[i - 1];
		if(T.id[i] <= n) rt[i] = tr.modify(rt[i - 1], 1, nn, a[T.id[i]]);
	}
	while(q--){
		int u = read(), x = read(), k = read();
		int v = T.anc(u, x);
		if(T.sz[v] < k){
			puts("-1");continue;
		} 
		printf("%d\n", b[tr.query(rt[T.ed[v]], rt[T.st[v] - 1], 1, nn, k)]);
	}
 	return 0;
}

Ⅲ. P4768 [NOI2018] 归程

我们需要现预处理出每个点到 1 号点的距离。

建立边权从大到小的 Kruskal 重构树。

找到车子能开过到的点, 显然都在可以转成一段连续的区间,用线段树来区间查询即可。

由于多组数据,一定要清空数组,不然直接 100 -> 5。(样例里居然没有多组数据)。

点击查看代码
#include<bits/stdc++.h>
#define ls u << 1
#define rs u << 1 | 1
using namespace std;
const int N = 5e5 + 67;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, m, q, lg, ans, T, k, s;
int dis[N], vis[N];
int tot, Head[N], Next[N << 1], to[N << 1], edge[N << 1];
void add(int u, int v, int w){
	to[++tot] = v, Next[tot] = Head[u], Head[u] = tot, edge[tot] = w;
}
struct Edge{
	int u, v, w;
	bool operator < (const Edge &I) const{return w > I.w;}
}A[N];
struct Tree{
	int tot, Head[N], Next[N << 1], to[N << 1];
	void add(int u, int v){
		to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
	}
	int fa[N], val[N], id[N], f[N][30], sz[N], dfn[N], st[N], ed[N], cnt, node;
	int find(int x){return fa[x] == x ? x : fa[x] = find(fa[x]);}
	void init(){
		for(int i = 1; i <= n; ++i) fa[i] = i; 
		node = n, cnt = 0, tot = 0;
		memset(Head, 0, sizeof(Head));
	}
	void merge(int u, int v, int w){
		if((v = find(v)) == (u = find(u))) return ;
		++node, fa[u] = fa[v] = fa[node] = node, val[node] = w;
		add(node, u), add(node, v);
	} 
	void dfs(int x){
		dfn[x] = ++cnt, id[cnt] = x, st[x] = cnt, sz[x] = 0;
		for(int i = Head[x]; i; i = Next[i]) dfs(to[i]), sz[x] += sz[to[i]], f[to[i]][0] = x;
		if(!sz[x]) sz[x] = 1; ed[x] = cnt;
	}
	void Build(int x){
		dfs(x);
		for(int i = 1; i <= lg; ++i)
			for(int j = 1; j <= node; ++j)
				f[j][i] = f[f[j][i - 1]][i - 1];
	}
	int anc(int u, int lim){  
		for(int i = lg; ~i; --i)
			if(f[u][i] && val[f[u][i]] >= lim)
			 	u = f[u][i];
		return u;
	}
}Kr;
struct SegmentTree{
	int minn[N << 2], cnt;
	void PushUp(int u){
		minn[u] = min(minn[ls], minn[rs]);
	}
	void Build(int u, int l, int r){
		if(l >= r) return minn[u] = dis[Kr.id[l]], void();
		int mid = (l + r) >> 1;
		Build(ls, l, mid), Build(rs, mid + 1, r);
		PushUp(u);
	}
	int Query(int u, int l, int r, int L, int R){
		if(L <= l && r <= R) return minn[u];
		int mid = (l + r) >> 1, ans = 0x3f3f3f3f;
		if(L <= mid) ans = min(ans, Query(ls, l, mid, L, R));
		if(R > mid) ans = min(ans, Query(rs, mid + 1, r, L, R));
		return ans; 
	}
}tr;
struct DIJ{
	int dis, s;
	bool operator < (const DIJ &A) const{return A.dis < dis;}
};
void dijkstra(int s){
	priority_queue<DIJ> q;
	memset(dis, 0x3f, sizeof(dis));
	memset(vis, 0, sizeof(vis));
	dis[s] = 0, q.push((DIJ){0, s});
	while(!q.empty()){
		int x = q.top().s; q.pop();
		if(vis[x]) continue; 
		vis[x] = 1;
		for(int i = Head[x]; i; i = Next[i]){
			int y = to[i];
			if(dis[y] > dis[x] + edge[i]){
				dis[y] = dis[x] + edge[i];
				q.push((DIJ){dis[y], y});
			}
		}
	}
}
int b[N], a[N];
int main(){
//	freopen("1.in", "r", stdin);
//	freopen("return.out", "w", stdout);
	T = read();
	while(T--){
		n = read(), m = read(), lg = log2(n) + 1, ans = 0;
		tot = 0; memset(Head, 0, sizeof(Head));
		for(int i = 1; i <= m; ++i){
			int u = read(), v = read(), l = read(), w = read();
			A[i] = (Edge){u, v, w}, add(u, v, l), add(v, u, l);
		}
		dijkstra(1);
		sort(A + 1, A + 1 + m); Kr.init();
		for(int i = 1; i <= m; ++i) Kr.merge(A[i].u, A[i].v, A[i].w);
		Kr.Build(Kr.node), tr.Build(1, 1, Kr.node);
		q = read(), k = read(), s = read();
		while(q--){
			int u = (read() + k * ans - 1) % n + 1, p = (read() + k * ans) % (s + 1);
			int v = Kr.anc(u, p + 1);
			printf("%d\n", ans = tr.Query(1, 1, Kr.node, Kr.st[v], Kr.ed[v]));
		}
	} 
 	return 0;
}

Ⅳ.CF1628E Groceries in Meteor Town

首先看到到两个点之间的简单路径中经过的最长的边,就去想是否可以用 边权从小到大的 Kruskal 重构树转化成两个点的 lca 的权值。

于是查询操作就转化成了在重构树上, \(x\) 点与所有白点的 \(lca\) 的权值。

再用一下 CF1062E Company 这题的经典结论:一个点集的 lca 为这个点集中 \(dfn\) 最大点和 \(dfn\) 最小点的 lca, 用线段树维护一下即可。

点击查看代码
#include<bits/stdc++.h>
#define ls u << 1
#define rs u << 1 | 1
using namespace std;
const int N = 6e5 + 67;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, q, lg;
struct Edge{
	int u, v, w;
	bool operator < (const Edge &A) const{return w < A.w;}
}A[N];
int fa[N], f[N][20], val[N], dfn[N], d[N], id[N], node, cnt;
int tot, Head[N], to[N << 1], Next[N << 1];
struct Krusal{
	void init(){for(int i = 1; i <= n; ++i) fa[i] = i; node = n;}
	int find(int x){return x == fa[x] ? x : fa[x] = find(fa[x]);}
	void add(int u, int v){
		to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
	}
	void merge(int u, int v, int w){
		if((u = find(u)) == (v = find(v))) return ;
		++node, fa[u] = fa[v] = fa[node] = node, val[node] = w;
		add(node, u), add(node, v);
	}
	void dfs(int x){
		dfn[x] = ++cnt, id[cnt] = x;
		for(int i = Head[x]; i; i = Next[i]) d[to[i]] = d[x] + 1, dfs(to[i]), f[to[i]][0] = x;
	}
	void build(int x){
		d[x] = 1, dfs(x);
		for(int i = 1; i <= lg; ++i)
			for(int j = 1; j <= node; ++j)
				f[j][i] = f[f[j][i - 1]][i - 1];
	}
}Kr;
int minn[N << 2], maxn[N << 2], mn[N << 2], mx[N << 2], lazy[N << 2];
void build(int u, int l, int r){
	mn[u] = 1e9, mx[u] = 0, lazy[u] = -1;
	if(l == r) return maxn[u] = minn[u] = dfn[l], void();
	int mid = (l + r) >> 1;
	build(ls, l, mid), build(rs, mid + 1, r);
	maxn[u] = max(maxn[ls], maxn[rs]), minn[u] = min(minn[ls], minn[rs]);
}
void update(int u, int t){
	t == 1 ? (mx[u] = maxn[u], mn[u] = minn[u]) : (mx[u] = 0, mn[u] = 1e9);
}
void pushdown(int u){
	lazy[ls] = lazy[rs] = lazy[u];
	update(ls, lazy[u]), update(rs, lazy[u]);
	lazy[u] = -1;
}
void modify(int u, int l, int r, int L, int R, int t){
	if(L <= l && r <= R) return update(u, t), lazy[u] = t, void();
	int mid = (l + r) >> 1;
	if(~lazy[u]) pushdown(u);
	if(L <= mid) modify(ls, l, mid, L, R, t);
	if(R > mid) modify(rs, mid + 1, r, L, R, t);
	mn[u] = min(mn[ls], mn[rs]), mx[u] = max(mx[ls], mx[rs]); 
}
int lca(int x, int y){
	if(d[x] < d[y]) swap(x, y);
	for(int i = lg; ~i; --i) if(d[f[x][i]] >= d[y]) x = f[x][i];
	if(x == y) return x;
	for(int i = lg; ~i; --i) if(f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
	return f[x][0];
}
int main(){
	n = read(), q = read(), lg = log2(n) + 1;
	for(int i = 1; i < n; ++i){
		int u = read(), v = read(), w = read();
		A[i] = (Edge){u, v, w};
	}
	sort(A + 1, A + n);
	Kr.init();
	for(int i = 1; i < n; ++i) Kr.merge(A[i].u, A[i].v, A[i].w);
	Kr.build(node); build(1, 1, n);
	while(q--){
		int opt = read(), l, r, x;
		if(opt <= 2){
			l = read(), r = read();
			modify(1, 1, n, l, r, opt == 1);
		}else{
			x = read();
			l = mn[1], r = mx[1];
			if(l == 1e9 || (l == dfn[x] && l == r)) puts("-1");
			else printf("%d\n", val[lca(id[l], lca(x, id[r]))]);
		}
	}
	return 0;
} 

完结撒花~

参考:
https://www.cnblogs.com/alex-wei/p/Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree.html

posted @ 2023-05-24 21:55  Aurora-JC  阅读(83)  评论(0编辑  收藏  举报