首页 > 其他分享 >【学习笔记】(15) Kruskal 重构树

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

时间:2023-05-24 22:00:25浏览次数:60  
标签:重构 ch 15 int Kruskal Tree dis

前置知识: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

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

标签:重构,ch,15,int,Kruskal,Tree,dis
From: https://www.cnblogs.com/jiangchen4122/p/17419355.html

相关文章

  • fastposter v2.15.0 从繁琐到简单,简洁好用的海报生成器
    fastposterv2.15.0从繁琐到简单,简洁好用的海报生成器从繁琐到简单,简洁好用的海报生成器我很高兴向大家推荐一款令人兴奋的工具——Fastposter海报生成器。作为一名开发者,我们深知在项目中创建专业级海报的重要性,但常常面临时间和设计技能的限制。现在,Fastposter海报生成器为我们......
  • fastposter v2.15.0 从繁琐到简单,简洁好用的海报生成器
    fastposterv2.15.0从繁琐到简单,简洁好用的海报生成器从繁琐到简单,简洁好用的海报生成器我很高兴向大家推荐一款令人兴奋的工具——Fastposter海报生成器。作为一名开发者,我们深知在项目中创建专业级海报的重要性,但常常面临时间和设计技能的限制。现在,Fastposter海报生成器为我......
  • 150万学术名词中英对照字典ACCESS数据库
    今天这个数据是一款字典的类型的软件,专门用来查询一些学术上面的名词的中英对照,超过180个学科分类,150多万条记录,伴随您悠游于学海之中,是您做学问、写论文的好帮手。主要科目有:電子計算機名詞(107213)、電機工程名詞(100395)、電力工程(68379)、外國地名譯名(64487)、機械工程(49872)、生......
  • upload-labs靶场第15关
    以上步骤和14关一样1 制作图片马 创建一个文件夹里面 准备一张 jpg图片和准备一个一句话木马的php文件 使用notepad++打开图片最后一行添加<?php phpinfo();?>2 打开cmd 使用命令制作图片马输入命令copy 图片位置 /b + 木马php位置 /a 编写的文件名称 .jpg3上传文......
  • XCZU15EG处理板设计原理图:(ZCU102E的pin兼容替代卡) 基于 XCZU15EG的双 FMC通用信号处
    (ZCU102E的pin兼容替代卡)基于XCZU15EG的双FMC通用信号处理板一、板卡概述   本板卡基于XilinxZynqUltrascale+MPSOC系列SOCXCZU15EG-FFVB1156架构,PS端搭载一组64-bitDDR4,容量32Gb,最高可稳定运行在2400MT/s,1路USB3.0接口、1路千兆网络接口、1路DP接口......
  • 【听书笔记-0515】-《清单革命》
    认识清单这是自樊登读书后第二次听清单革命这本书,个人认为还是收获很大的。清单最大的魅力是使用去强制性外力督促人去注意细节,变得慎重。我们每个人都会犯错,通常是连个原因造成的:无知或者无能。无知是因为我们记忆力是有限资源,不可能知道、记住所有的知识; 无能是由于人的注意力也......
  • leetcode2215哈希列表的应用
    哈希列表存储的是不重复的元素,使用两个哈希列表存储这两个数组。再遍历其中一个哈希列表,查看是否存在另一个哈希列表中set.insert()set1.count(元素)for(intnum:nums1){set1.insert(num);}for(intnum:set1){if(!set2.count(num)){res[0].push_back(num);......
  • poj-1519
    //132K0MSC++#include<cstdio>#include<cstring>usingnamespacestd;longlonggetDigitSum(longlongval){longlongdigitSum=0;if(val<10){returnval;}else{while(val){digitSum+=......
  • 2、yum安装postgres15.3
    目录yum安装postgres15.31、选择安装的版本1.532、创建postgres用户3、执行yum安装命令4、修改配置文件4.1、修改postgresql.conf4.2、修改pg_hba.confyum安装postgres15.31、选择安装的版本1.53参考官网文档:https://www.postgresql.org/download/linux/redhat/2、创建postgr......
  • 图解LeetCode——剑指 Offer 15. 二进制中1的个数
    一、题目编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为'1'的个数(也被称为 汉明重量)。二、示例2.1>示例1:【输入】n=11(控制台输入00000000000000000000000000001011)【输出】3【解释】输入的二进制串000000000000000000000000000010......