最小生成树

\(Prim\):适用于稠密图,也可以使用类似于\(Dijkstra\)的优化方式。
\(Kruskal\):本质上是一种贪心,利用贪心的性质可解决一些最优化问题。
\(Boruvka\):一般在具有特殊性质的完全图中配合数据结构使用。
以上三种算法,是解决最小生成树问题的基础,请读者务必详细了解每种算法的本质原理。
一道用\(Prim\)\(Kruskal\)算法难以解决,但\(Boruvka\)算法容易解决的题目。
\(CF1550F\)
https://www.luogu.com.cn/problem/CF1550F
题意:数轴上\(n\)个点,初始在点\(s\),有一个常量\(d\)和一个变量\(k\),点\(i\)能跳到点\(j\),当且仅当\(d-k\leq |a_i-a_j|\leq d+k\)
多次询问\(k,x\),回答在变量为\(k\)的情况下,能否从\(s\)\(x\)
题解:边\((i,j)\)的存在性关于\(k\)显然存在单调性,于是考虑令\(t(i,j)\)为最小的\(k\)使得边\((i,j)\)存在。
我们要找一条从\(s\)\(x\)路径,使得路径上边最小值大于等于\(k\),显然是让最小值最大,即瓶颈路。
\(t(i,j)\)从大到小建立生成树,树上\(s\)\(x\)路径的边权最小值,即为所求路径。
考虑完全图中用\(Boruvka\)求最短路。
本题中要求对于\(i\)找到一个\(j\),满足\(i,j\)当前不在同一连通块,且\(||a_i-a_j|-d|\)最小。
\(i>j,||a_i-a_j|-d|=|a_i-d-a_j|\)
\(i<j,||a_i-a_j|-d|=|a_j-a_i-d|=|a_i+d-a_j|\)
于是用\(set\)存点,查询某连通块时,先在\(set\)中删除连通块内所有点,然后二分查找即可,查询完将连通块内点重新加入\(set\)即可。

#include <bits/stdc++.h>

using namespace std;

const int N=1e6+10;

int n,m,s,d,cnt;
int p[N],a[N],id[N],d1[N];
int h[N],e[N],w[N],ne[N],idx;
bool st[N];
set<int> S;
vector<int> q[N];

void add(int a,int b,int c){
	e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

int find(int x){
	if(x==p[x]) return x;
	return find(p[x]);
}

void merge(int x,int y,int z){
	x=find(x),y=find(y);
	if(x==y) return;
	p[x]=y;
	add(x,y,z),add(y,x,z);
}

void dfs(int u,int fa,int dist){
	d1[u]=dist,st[u]=1,q[cnt].push_back(u);
	for(int i=h[u]; i!=-1; i=ne[i]){
		int j=e[i];
		if(j==fa) continue;
		dfs(j,u,max(dist,w[i]));
	}
}

int main(){
	memset(h,-1,sizeof h);
	cin >> n >> m >> s >> d;
	for(int i=1; i<=n; i++) cin >> a[i];
	for(int i=1; i<=n; i++) id[a[i]]=i;
	for(int i=1; i<=n; i++) p[i]=i;
	for(int i=1; i<=n; i++) S.insert(a[i]);
	while(1){
		for(int i=1; i<=cnt; i++){
			for(int j=0; j<q[i].size(); j++) S.erase(a[q[i][j]]);
			int mi=1e9,pos;
			for(int j=0; j<q[i].size(); j++){
				set<int>::iterator it;
				it=S.lower_bound(a[q[i][j]]-d);
				if(it!=S.end())
					if(id[*it]<q[i][j])
						if(abs(abs(a[q[i][j]]-(*it))-d)<mi)
							mi=abs(abs(a[q[i][j]]-(*it))-d),pos=id[*it];
				if(it!=S.begin()){
					it--;
					if(id[*it]<q[i][j])
						if(abs(abs(a[q[i][j]]-(*it))-d)<mi)
							mi=abs(abs(a[q[i][j]]-(*it))-d),pos=id[*it];
				}
				it=S.lower_bound(a[q[i][j]]+d);
				if(it!=S.end())
					if(id[*it]>q[i][j])
						if(abs(abs(a[q[i][j]]-(*it))-d)<mi)
							mi=abs(abs(a[q[i][j]]-(*it))-d),pos=id[*it];
				if(it!=S.begin()){
					it--;
					if(id[*it]>q[i][j])
						if(abs(abs(a[q[i][j]]-(*it))-d)<mi)
							mi=abs(abs(a[q[i][j]]-(*it))-d),pos=id[*it];
				}
			}
			merge(q[i][0],pos,mi);
			for(int j=0; j<q[i].size(); j++) S.insert(a[q[i][j]]);
		}
		memset(st,0,sizeof st);
		cnt=0;
		for(int j=1; j<=n; j++)
		if(!st[j]) q[++cnt].clear(),dfs(j,-1,0);	
		if(cnt==1) break;
	}
	dfs(s,-1,0);
	for(int i=1; i<=m; i++){
		int x,y;
		cin >> x >> y;
		if(d1[x]<=y) cout << "YES" << endl;
		else cout << "NO" << endl; 
	}
	return 0;
}

\(Kruskal\)重构树是在\(Kruskal\)求最小生成树过程中,将每次加入一条树边,改为增加一个新点,点权存储这条边的信息,并从新点向树边两端点所处子树根连边。这样可将两点间信息,转化为两点\(lca\)的点权信息。
一道\(Kruskal\)重构树同最短路相结合的例题。
\(P4768\)
https://www.luogu.com.cn/problem/P4768
题意:过于冗长,自行观看。
题解:先求出\(1\)号点到所有点的最短路,对于每次询问,考虑枚举停车点。若当天起点为\(x\),可用停车点\(y\)要满足,存在一条\(x\sim y\)路径,使得路径最低海拔大于等于\(p\),即最大化最小边权。考虑\(Kruskal\)重构树,重构树上从\(x\)向上跳,跳到满足\(p\leq a_k\),且深度最小的点\(k\)。回答以\(k\)为根子树的叶子节点中,到\(1\)距离最小的点即可。

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<LL,int> PLI;
const int N=2e6+10;

int n,m,q,k,s,lg,root;
int p[N],dep[N],high[N],f[N][22];
int h[N],e[N],w[N],ne[N],idx;
bool st[N];
LL d[N];
struct Edge {
	int x,y,z;
} E[N];

bool operator <(Edge a,Edge b) {
	return a.z<b.z;
}

int find(int x) {
	if(x!=p[x]) p[x]=find(p[x]);
	return p[x];
}

void add(int a,int b,int c) {
	e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

void dijkstra() {
	for(int i=1; i<=n; i++) st[i]=0;
	for(int i=1; i<=2*n; i++) d[i]=1e16;
	priority_queue<PLI,vector<PLI>,greater<PLI>> heap;
	d[1]=0,heap.push({d[1],1});
	while(heap.size()) {
		int t=heap.top().second;
		heap.pop();
		if(st[t]) continue;
		st[t]=1;
		for(int i=h[t]; i!=-1; i=ne[i]) {
			int j=e[i];
			if(st[j]) continue;
			if(d[t]+w[i]<d[j]) {
				d[j]=d[t]+w[i];
				heap.push({d[j],j});
			}
		}
	}
}

void dfs(int u,int depth) {
	dep[u]=depth;
	if(u>=1&&u<=n) return;
	for(int i=h[u]; i!=-1; i=ne[i]) {
		int j=e[i];
		f[j][0]=u;
		for(int k=1; k<=lg; k++)
			f[j][k]=f[f[j][k-1]][k-1];
		dfs(j,depth+1);
		d[u]=min(d[u],d[j]);
	}
}

int get(int x,int y) {
	for(int i=lg; i>=0; i--)
		if(high[f[x][i]]>y) x=f[x][i];
	return d[x];
}

int main() {
	int T;
	cin >> T;
	while(T--) {
		cin >> n >> m;
		lg=(int)(log(n)/log(2))+1;
		idx=0;
		for(int i=1; i<=2*n; i++) h[i]=-1;
		for(int i=1; i<=m; i++) {
			int x,y,z,h;
			cin >> x >> y >> z >> h;
			add(x,y,z),add(y,x,z);
			E[i]= {x,y,h};
		}
		dijkstra();
		sort(E+1,E+m+1);
		reverse(E+1,E+m+1);
		for(int i=1; i<=2*n; i++) p[i]=i;
		root=n;
		for(int i=1; i<=m; i++) {
			int x=E[i].x,y=E[i].y,z=E[i].z;
			x=find(x),y=find(y);
			if(x==y) continue;
			high[++root]=z,p[x]=p[y]=root;
			add(root,x,0),add(root,y,0);
		}
		for(int i=0; i<=lg; i++) f[root][i]=0;
		dfs(root,1);
		cin >> q >> k >> s;
		for(int i=1,last=0; i<=q; i++) {
			int v,p;
			cin >> v >> p;
			v=(v+k*last-1)%n+1;
			p=(p+k*last)%(s+1);
			last=get(v,p);
			cout << last << endl;
		}
	}
	return 0;
}

一道不寻常的\(Kruskal\)重构树题
\(P6765\)
https://www.luogu.com.cn/problem/P6765
题意:过于冗长,自行观看。
题解:传统的\(Kruskal\)重构树是在连树边,即连接两集合时,建立新点,于是两集合间点对路径的最值信息,转化为两点\(lca\)处信息。在本题中仍然用这种思想,不过建立新点的条件有所变换。
考虑两点间可达的条件,即两点间连通,且所处连通块非链。于是考虑维护连通块是否非链,若为链,用\(l,r\)表示链的左右端点。
同样按边权从小到大枚举边,关键信息是令连通块变成非链的那条边,于是考虑在连通块变为非链时候建新点,新点向连通块内所有点连边。
于是用并查集维护叶子节点连通性,当连通块变为非链连通块时,或非链连通块之间连通时,建立新点,新点连向变成非链连通块内的所有点,和原本处于非链连通块的根节点。这样做既维护同时连通且非链的性质。

#include <bits/stdc++.h>

using namespace std;

const int N=5e5+10;

int n,m,lg;
int p[N],sz[N],pos[N][2];
int a[N],rt[N],dep[N],f[N][22];
bool st[N],st1[N];
vector<int> q[N],tr[N];

struct Edge{
	int x,y,z;
	bool operator <(Edge t){
		return z<t.z;
	}
}E[N];

struct Dsu{
	void init(){
		for(int i=1; i<=n; i++) p[i]=i;
	}
	int find(int x){
		if(x==p[x]) return x;
		return p[x]=find(p[x]);
	}
}dsu;

void dfs(int u,int fa,int depth){
	dep[u]=depth;
	if(fa==-1) rt[u]=u;
	else rt[u]=rt[fa]; 
	if(u<=n) return;
	for(int i=0; i<tr[u].size(); i++){
		int j=tr[u][i];
		f[j][0]=u;
		for(int k=1; k<=lg; k++)
			f[j][k]=f[f[j][k-1]][k-1];
		//cout << u << " " << j << " OK" << endl;
		dfs(j,u,depth+1);
	}
}

int lca(int x,int y){
	//cout << x << " " << y << " " << rt[x] << " " << rt[y] << endl;
	if(rt[x]!=rt[y]) return -1;
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=lg; i>=0; i--)
		if(dep[f[x][i]]>=dep[y]) x=f[x][i];
	if(x==y) return x;
	for(int i=lg; i>=0; i--)
		if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
	return f[x][0];
}

void init(int N,int M,vector<int> U,vector<int> V,vector<int> W){
	n=N,m=M;
	lg=(int)(log(2*n)/log(2))+1;
	for(int i=0; i<m; i++){
		int x=U[i],y=V[i],z=W[i];
		x++,y++;
		E[i+1]=(Edge){x,y,z};
	}
	sort(E+1,E+m+1);
	dsu.init();
	for(int i=0; i<500000; i++) pos[i][0]=pos[i][1]=rt[i]=i,sz[i]=1,q[i].push_back(i);
	int num=n;
	for(int i=1; i<=m; i++){
		int x=E[i].x,y=E[i].y,z=E[i].z,tx=x,ty=y;
		x=dsu.find(x),y=dsu.find(y);
		if(q[x].size()>q[y].size()) swap(x,y),swap(tx,ty);
		if(x==y){
			if(!st[x]){
				st[x]=1;
				a[++num]=z;
				for(int j=0; j<q[x].size(); j++)
					tr[num].push_back(q[x][j]);
				rt[x]=num;
				q[x].clear();
			}
			continue;
		}
		if(st[x]&&st[y]){
			a[++num]=z;
			tr[num].push_back(rt[x]);
			tr[num].push_back(rt[y]);
			p[x]=y,rt[x]=rt[y]=num;
			continue;
		}
		if(st[x]&&!st[y]){
			st[y]=1;
			a[++num]=z;
			tr[num].push_back(rt[x]);
			for(int j=0; j<q[y].size(); j++)
				tr[num].push_back(q[y][j]);
			q[y].clear();
			rt[y]=num;
			p[x]=y;
			continue;
		}
		if(!st[x]&&st[y]){
			st[x]=1;
			a[++num]=z;
			tr[num].push_back(rt[y]);
			for(int j=0; j<q[x].size(); j++)
				tr[num].push_back(q[x][j]);
			q[x].clear();
			rt[y]=num;
			p[x]=y;
			continue;
		}
		bool flag=0;
		for(int i=0; i<2; i++)
			for(int j=0; j<2; j++)
				for(int k=0; k<2; k++){
					if(flag) continue;
					if(k%2) swap(tx,ty);
					if(pos[x][i]==tx&&pos[y][j]==ty){
						flag=1;
						int Tx=pos[x][1-i],Ty=pos[y][1-j];
						for(int t=0; t<q[x].size(); t++)
							q[y].push_back(q[x][t]);
						q[x].clear();
						p[x]=y;
						pos[y][0]=Tx,pos[y][1]=Ty;
						
					}
					if(k%2) swap(tx,ty);
				}
		if(flag) continue;
		a[++num]=z;
		for(int j=0; j<q[x].size(); j++)
			tr[num].push_back(q[x][j]);
		q[x].clear();
		for(int j=0; j<q[y].size(); j++)
			tr[num].push_back(q[y][j]);
		q[y].clear();
		st[x]=st[y]=1;
		rt[y]=num;
		p[x]=y;
	}
	//for(int i=0; i<q[8].size(); i++) cout << q[8][i] << " OK";
	//cout << endl;
	for(int i=num; i; i--) if(!dep[i]) dfs(i,-1,1);
}

int getMinimumFuelCapacity(int X,int Y){
	int x=X,y=Y;
	x++,y++;
	int p=lca(x,y);
	if(p==-1) return -1;
	return a[p];
}

int main(){
	int n,m;
	vector<int> U,V,W;
	cin >> n >> m;
	for(int i=1; i<=m; i++){
		int x,y,z;
		cin >> x >> y >> z;
		U.push_back(x);
		V.push_back(y);
		W.push_back(z);
	}
	init(n,m,U,V,W);
	int q;
	cin >> q;
	for(int i=1; i<=q; i++){
		int x,y;
		cin >> x >> y;
		cout << getMinimumFuelCapacity(x,y) << endl;
	}
	return 0;
}
posted @ 2025-02-25 09:06  junliang123  阅读(92)  评论(0)    收藏  举报