最小生成树

算法理解

最小生成树用到了一个贪心策略:图上最小的边一定在最小生成树上(MST),证法选取三个点,手模一下,很显然

Kruskal算法

最小的边一定在MST上,每次选取最小的边,添加到MST中,再判圈,若加入这条边形成圈,则不合法,可以用并查集实现,复杂度瓶颈为排序 \(O(mlogm)\)

代码

#include<bits/stdc++.h>
using namespace std;
const int N=305,M=1e5+5;
int n,m,u,v,w,ans;
int fa[N];
struct edge{
	int u,v,w;
}b[M];
bool cmp(edge x,edge y){
	return x.w<y.w;
}
int find(int x){
	if(fa[x]==x)  return x;
	return fa[x]=find(fa[x]);
}
bool merge(int x,int y){
	x=find(x),y=find(y);
	if(x==y)  return 0;
	fa[x]=y;
	return 1;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&u,&v,&w);
		b[i]={u,v,w};
	}
	for(int i=1;i<=n;i++)  fa[i]=i;
	sort(b+1,b+1+m,cmp);
	for(int i=1;i<=m;i++){
		if(merge(b[i].u,b[i].v))  ans=max(b[i].w,ans);
	}
	printf("%d %d",n-1,ans);
}

Prim算法

和dijkstra很像,通往最近的邻居的路一定在MST上,证法如下图

然后,我们每次找到一个不在MST上的点,就把它以与MST的距离为关键字加入堆中(注:一个点可以被加入多次,因为点到MST的距离可能由新点的加入而改变),复杂度为每一次确定一条边的N乘上每次堆排序的复杂度,均摊下来有m次进入堆的操做,数在堆中的量级是m的,复杂度 \(O((n+m)logm)\)

代码

#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N=305;
int n,m,u,v,w,ans1,ans2;
int vis[N];
vector<pii>b[N];
priority_queue<pii,vector<pii>,greater<pii> >q;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&u,&v,&w);
		b[u].push_back({v,w});
		b[v].push_back({u,w});
	}
	q.push({0,1});
	while(!q.empty()){
		int k=q.top().second,val=q.top().first;
		q.pop();
		if(vis[k])  continue;
		vis[k]=1;
		ans1+=val;
		ans2=max(ans2,val);
		for(auto i:b[k]){
			int to=i.first,c=i.second;
			if(vis[to])  continue;
			q.push({c,to});
		}
	}
	printf("%d %d",n-1,ans2);
}

应用

最长路径

kruskal将排序大于号改小于号即可
prim?众所周知dij是跑不了最长路的,因为其有一个松弛操作,不符合贪心策略,然而prim就不一样了,它没有松弛操作,并且在我上文的证明中是成立的,所以可行

严格次小生成树

我们先考虑一个非严格次小生成树,就是求这张图的最小生成树是否唯一,我们考虑若不唯一则一定有一条等长的边,但不一定有等长的边就会有非严格次小MST,我们枚举每一条不是树边的边,若加入这条边形成的圈上有和这条边相等的边则说明存在
怎么判断呢,只需要用倍增维护一下 \(lca(u,v)\) 上边的最大值即可,为啥是最大值,因为你加入的这条边若比圈内某一条树边要小,那么加入的就不是那条树边了,而是这条边
严格次小生成树,只需要将判断等于改成替换掉这条树边就行了

P4951 Earthquake

递一份题解吧,讲的比我明白

ybtoj 3.2最小生成树

3.2.1

板子题

3.2.2

考虑贪心,取最小的点为发电站,然后跑一遍最小生成树,然后看起来非常的对啊,喜提50分
为什么呢,因为可能并没有建立两个发电站,然后分别供应更优,所以我们建立超级原点,将原点到各个基站的代价改为,建立发电站所需的费用即可

3.2.3

由于要求动态加边,所以我们先考虑暴力没加一条边都做一次最小生成树,prim的瓶颈在堆维护,这个不好搞,而kruskal的复杂度瓶颈在排序,每次加入一条边都要排一遍序,考虑只新加入一条边,前面边的大小都是有序的,所以只需要加入一条边进行冒泡排序即可

3.2.4

考虑每加入一条边将两连通块相连时带来的贡献,注意,因为有且仅有一棵MST,所以由这条树边构造出来的边要比这条树边+1

3.2.5

ybt题解讲的很明白,太巧妙了

3.2.6

这篇的转化也很巧妙,但是我没看懂连通代表什么,所以我证明了一下,在下方图片中


注意本题的输入,本蒟蒻在这里卡了好久

3.2.7

仔细想,题目中给的约束条件相当于让你求一棵树,再仔细一想,完全符合最小生成树的定义,最后让你求一下每个点在最小生成树上的父亲,用prim做直接板子题,维护一下父亲即可

3.2.8

其实3.2.3最优解应该就是这种方法,但我没有想到
就是从小到大枚举g,然后针对剩下的边跑一遍最小生成树,考虑优化,针对已经形成的最小生成树,边的最大值只会越来越小,所以只有新加入的边才会对最小生成树产生影响,所以只需要对这部分进行最小生成树即可
本题代码实现难度于我而言很高,特意奉上代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5,inf=1e18+9;
int n,m,mg,ms,u,v,g,s,ans=inf,top;
int fa[N],vis[N],a[N];
struct edge{
	int u,v,g,s;
}b[N];
bool cmp(edge x,edge y){
	return x.g<y.g;
}
int find(int x){
	if(x==fa[x])  return x;
	return fa[x]=find(fa[x]);
}
bool merge(int x,int y){
	x=find(x),y=find(y);
	if(x==y)  return 0;
	fa[x]=y;
	return 1;
}
signed main(){
	scanf("%lld%lld",&n,&m);
	scanf("%lld%lld",&mg,&ms);
	for(int i=1;i<=m;i++){
		scanf("%lld%lld%lld%lld",&u,&v,&g,&s);
		b[i]={u,v,g,s};
	}
	sort(b+1,b+1+m,cmp);
	for(int i=1;i<=m;i++){
		a[++top]=i;
		int jj=top-1;
		while(jj&&b[a[jj]].s>b[a[jj+1]].s)  swap(a[jj],a[jj+1]),jj--;
		for(int j=1;j<=n;j++)  fa[j]=j;
		int tot=0,num=0;
		for(int j=1;j<=top;j++){
			if(merge(b[a[j]].u,b[a[j]].v)){
				tot=max(tot,b[a[j]].s);
				a[++num]=a[j];
			}  
		}
		if(num==n-1)  ans=min(ans,mg*b[i].g+ms*tot);
		top=num;
	}
	if(ans==inf)  printf("-1");
	else  printf("%lld",ans);
}

3.2.9

和上面P4951,是同一题

posted @ 2024-10-27 21:01  daydreamer_zcxnb  阅读(41)  评论(0编辑  收藏  举报