$Kruskal$ 重构树

参考 \(Kruskal\) 构建最小生成树的过程,把边按权升序排序,用并查集判断连通性,这里并查集不能做按秩合并。

考虑一条边 \([u,v,w]\),如果 \(u\)\(v\) 属于同一连通分量,跳过;否则新建一个虚拟点,虚拟点的点权为 \(w\),把 \(p[u]\)\(p[v]\) 指向这个虚拟点,同时添加从虚拟点到 \(u\)\(v\) 的有向边。可以发现,\(Kruskal\) 重构树中的叶子节点为原图的节点。


1.https://www.luogu.com.cn/problem/P2245

题意概述:给定一张带权无向图,有若干询问,每个询问求出 从 \(u\)\(v\) 路径上边权最大值的可能最小值。


构建 \(Kruskal\) 重构树,相当于询问求 \(u\)\(v\) 的最近公共祖先的权值,在构建完树后 \(dfs\) 一次维护深度信息和倍增表信息即可,之后就是标准的求 \(LCA\)。这样可以做到在线查询,每次查询 \(\mathcal{O}(n \log n)\)

//author:kzssCCC

#include <bits/stdc++.h>
using namespace std;
using ll = long long;


void solve(){
	int n,m;
	cin >> n >> m;

	vector<array<int,3>> edges(m);
	for (int i=0;i<m;i++){
		int u,v,w;
		cin >> u >> v >> w;

		edges[i] = {u,v,w};
	}

	sort(edges.begin(),edges.end(),[](auto& p1,auto& p2){
		return p1[2] < p2[2];
	});
	
	vector<int> p(n+1);
	iota(p.begin(),p.end(),0);
	vector<int> val(n+1,0);

	vector<vector<int>> adj(n+1);

	int tot = 1;

	auto find = [&](int u){
		int v = u;
		while (v!=p[v]){
			v = p[v];
		}

		while (u!=v){
			int next = p[u];
			p[u] = v;
			u = next;
		}

		return v;
	};		

	for (auto& [u,v,w]:edges){
		int a = find(u);
		int b = find(v);

		if (a==b) continue;

		p.push_back(n+tot);
		val.push_back(w);
		adj.push_back({a,b});
		p[a] = n+tot;
		p[b] = n+tot;

		tot++;
	}

	n = n+tot-1;

	vector<int> lg(n+1,0);
	for (int i=2;i<=n;i++){
		lg[i] = lg[i>>1]+1;
	}

	int K = lg[n];
	vector<vector<int>> dp(n+1,vector<int>(K+1,-1));
	vector<int> depth(n+1,0);

	function<void(int)> dfs = [&](int u){
		for (auto& v:adj[u]){
			depth[v] = depth[u]+1;
			dp[v][0] = u;

			dfs(v);
		}
	};	

	for (int i=1;i<=n;i++){
		if (p[i]==i){
			dfs(i);
		}
	}

	for (int k=1;k<=K;k++){
		for (int i=1;i<=n;i++){
			if (dp[i][k-1]==-1) continue;

			dp[i][k] = dp[dp[i][k-1]][k-1];
		}
	}

	auto getlca = [&](int a,int b){
		if (depth[a]<depth[b]){
			swap(a,b);
		}

		int diff = depth[a]-depth[b];

		for (int k=K;k>=0;k--){
			if (diff>>k&1){
				a = dp[a][k];	
			}
		}

		if (a==b) return a;

		for (int k=K;k>=0;k--){
			if (dp[a][k]!=dp[b][k]){
				a = dp[a][k];
				b = dp[b][k];
			}
		}

		return dp[a][0];
	};

	int q;
	cin >> q;

	while (q--){
		int a,b;
		cin >> a >> b;

		if (find(a)!=find(b)){
			cout << "impossible" << '\n';
		}
		else{
			cout << val[getlca(a,b)] << '\n';
		}
	}
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int t = 1;
	// cin >> t;
	while (t--) solve();

	return 0;
}

2.https://www.luogu.com.cn/problem/P9638

在建树的时候要标记改边对应的点的编号,统计 \(size\) 信息即可,注意所有操作3会在操作1统一生效,由于题目保证了修改操作所有边权的相对大小不发生变化,这题才可以用 \(Kruskal\) 重构树。


//author:kzssCCC

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int INF = 1e9;

void solve(){
	int n,m,q;
	cin >> n >> m >> q;

	vector<array<int,3>> edges(m);
	for (int i=0;i<m;i++){
		int u,v,w;
		cin >> u >> v >> w;

		edges[i] = {w,u,v};
	}

	vector<int> ord(m);
	iota(ord.begin(),ord.end(),0);
	sort(ord.begin(),ord.end(),[&](int i,int j){
		return edges[i][0] > edges[j][0];
	});

	vector<int> ref(m,0);
	vector<int> val(n+1,0);
	vector<int> p(n+1);
	iota(p.begin(),p.end(),0);
	int tot = 1;

	vector<vector<int>> adj(n+1);

	auto find = [&](int u){
		int v = u;
		while (p[v]!=v){
			v = p[v];
		}

		while (u!=v){
			int next = p[u];
			p[u] = v;
			u = next;
		}

		return v;
	};

	for (int i=0;i<m;i++){
		auto& [w,u,v] = edges[ord[i]];

		int a = find(u);
		int b = find(v);

		if (a==b) continue;

		val.push_back(w);
		p.push_back(n+tot);
		ref[ord[i]] = n+tot;
		adj.push_back({a,b});

		p[a] = n+tot;
		p[b] = n+tot;
		tot++;
	}

	n += tot-1;

	vector<int> sz(n+1,0);
	vector<int> lg(n+1,0);

	for (int i=2;i<=n;i++){
		lg[i] = lg[i>>1]+1;
	}

	int K = lg[n];

	vector<vector<int>> dp(n+1,vector<int>(K+1,-1));

	function<void(int)> dfs = [&](int u){
		if (u>=1 && u<=n-tot+1){
			sz[u] = 1;
		}

		for (auto& v:adj[u]){
			dp[v][0] = u;

			dfs(v);
			sz[u] += sz[v];
		}
	};

	for (int i=1;i<=n;i++){
		if (p[i]==i){
			dfs(i);
		}
	}

	for (int k=1;k<=K;k++){
		for (int i=1;i<=n;i++){
			if (dp[i][k-1]==-1) continue;

			dp[i][k] = dp[dp[i][k-1]][k-1];
		}
	}

	int lim = -INF;
	vector<pair<int,int>> wait;

	while (q--){
		int op;
		cin >> op;

		if (op==1){
			int x;
			cin >> x;
			lim = x;

			for (auto& [u,w]:wait){
				val[u] = w;
			}

			wait.clear();
		}
		else if (op==2){
			int x;
			cin >> x;

			for (int k=K;k>=0;k--){
				if (dp[x][k]!=-1 && val[dp[x][k]]>=lim){
					x = dp[x][k];
				}
			}

			cout << sz[x] << '\n';
		}
		else{
			int x,y;
			cin >> x >> y;
			x--;

			if (ref[x]!=0){
				wait.emplace_back(ref[x],y);
			}
		}
	}
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int t = 1;
	// cin >> t;
	while (t--) solve();

	return 0;
}

3.https://atcoder.jp/contests/agc002/tasks/agc002_b

题意概述:给定一张连通无向图,若干查询给定 \(x\)\(y\)\(z\),两个人分别从 \(x\)\(y\),一共需要走过 \(z\) 个不同的节点,求经过边权最大值的最小可能值,边权为边的编号。


建树,维护深度和倍增表信息。对于每个询问,可以二分最大的边权,只要 \(x\)\(y\) 在重构树向上跳跃经过原图的不同节点数量超过 \(z\) 就判断为 \(true\),找到最小的最大边权即可。
具体实现上,先对 \(x\)\(y\)\(LCA\),记为 \(g\),如果 \(g\) 的点权严格大于 \(mid\),说明到不了这个点,那么从 \(x\)\(y\) 各自向上跳跃即可,否则直接从 \(g\) 开始向上跳跃,这样就能保证不会统计到重复的点。


//author:kzssCCC

#include <bits/stdc++.h>
using namespace std;
using ll = long long;


void solve(){
	int n,m;
	cin >> n >> m;

	vector<pair<int,int>> edges(m+1);
	for (int i=1;i<=m;i++){
		int u,v;
		cin >> u >> v;

		edges[i] = {u,v};
	}

	vector<int> p(n+1);
	iota(p.begin(),p.end(),0);
	vector<int> val(n+1,0);
	vector<vector<int>> adj(n+1);

	int tot = 1;

	auto find = [&](int u){
		int v = u;
		while (p[v]!=v){
			v = p[v];
		}

		while (u!=v){
			int next = p[u];
			p[u] = v;
			u = next;
		}

		return v;
	};

	for (int i=1;i<=m;i++){
		auto& [u,v] = edges[i];

		int a = find(u);
		int b = find(v);

		if (a==b) continue;

		val.push_back(i);
		p.push_back(n+tot);
		adj.push_back({a,b});

		p[a] = n+tot;
		p[b] = n+tot;

		tot++;
	}

	n += tot-1;
	vector<int> sz(n+1,0);
	vector<int> lg(n+1,0);

	for (int i=2;i<=n;i++){
		lg[i] = lg[i>>1]+1;
	}

	int K = lg[n];
	vector<vector<int>> dp(n+1,vector<int>(K+1,-1));
	vector<int> depth(n+1,0);

	function<void(int)> dfs = [&](int u){
		if (adj[u].empty()){
			sz[u] = 1;
			return;
		}

		for (auto& v:adj[u]){
			dp[v][0] = u;
			depth[v] = depth[u]+1;

			dfs(v);
			sz[u] += sz[v];
		}
	};

	for (int i=1;i<=n;i++){
		if (p[i]==i){
			dfs(i);
		}
	}

	for (int k=1;k<=K;k++){
		for (int i=1;i<=n;i++){
			if (dp[i][k-1]==-1) continue;

			dp[i][k] = dp[dp[i][k-1]][k-1];
		}
	}

	auto getlca = [&](int a,int b){
		if (depth[a]<depth[b]){
			swap(a,b);
		}

		int diff = depth[a]-depth[b];
		for (int k=K;k>=0;k--){
			if (diff>>k&1){
				a = dp[a][k];
			}
		}

		if (a==b) return a;

		for (int k=K;k>=0;k--){
			if (dp[a][k]!=dp[b][k]){
				a = dp[a][k];
				b = dp[b][k];
			}
		}

		return dp[a][0];
	};

	int q;
	cin >> q;

	while (q--){
		int x,y,z;
		cin >> x >> y >> z;

		auto check = [&](int mid){
			auto cal = [&](int u){
				for (int k=K;k>=0;k--){
					if (dp[u][k]!=-1 && val[dp[u][k]]<=mid){
						u = dp[u][k];
					}
				}

				return sz[u];
			};

			int r = getlca(x,y);

			if (val[r]<=mid){
				for (int k=K;k>=0;k--){
					if (dp[r][k]!=-1 && val[dp[r][k]]<=mid){
						r = dp[r][k];
					}
				}

				return sz[r]>=z;
			}
			else{
				return cal(x)+cal(y)>=z;
			}
		};

		int l=1,r=m;
		while (l<=r){
			int mid = l+r >> 1;

			if (check(mid)){
				r = mid-1;
			}
			else{
				l = mid+1;
			}
		}

		cout << l << '\n';
	}
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int t = 1;
	// cin >> t;
	while (t--) solve();

	return 0;
}
posted @ 2026-04-13 16:24  kzssCCC  阅读(5)  评论(0)    收藏  举报