c++ 最小生成树

关于最小生成树

最小生成树,简写为 MST
相信大家一定记得这样一个定理:把 N 个点用 N-1 条边连接,形成的连通块一定是一棵树
当然一个 N 个点联通图肯定有大于等于 N-1 条边,而最小生成树就是从中选 N-1 条边以联通 N 个点
并且这 N-1 条边的边权和是所有方案中最小的

Kruskal

Kruskal 基于贪心和并查集,按边权从小到大加入,前提是不形成环
具体看代码

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
inline char gc(){
	static char buf[100000],*S=buf,*T=buf;
	return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
}
inline int read(){
    static char c=gc();register int f=1,x=0;
    for(;c>'9'||c<'0';c=gc()) c==45?f=-1:1;
    for(;c>'/'&&c<':';c=gc()) x=(x<<3)+(x<<1)+(c^48);
    return x*f;
}
struct tr{int u,v;LL val;}t[400002];
int n,m;
int f[100002],h[100002],st,ed,tot=0;LL ans=0;
inline int find(int x){while(f[x]!=x) x=f[x]=f[f[x]];return x;}
inline void merge(int x,int y){
	if(h[x]==h[y]) ++h[x],f[y]=x;
	else if(h[x]<h[y]) f[x]=y;
	else f[y]=x;
} 
void qs(int l,int r){
	int i=l,j=r;tr mid=t[rand()%(r-l)+l];
	while(i<=j){
		while(t[i].val<mid.val) i++;
		while(t[j].val>mid.val) j--;
		if(i<=j){
			swap(t[i],t[j]);
			i++,j--;
		}
	}
	if(i<r) qs(i,r);
	if(j>l) qs(l,j);
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++) f[i]=i;
	for(int i=1;i<=m;i++) t[i].u=read(),t[i].v=read(),t[i].val=1ll*read();
	qs(1,m);
	for(int i=1;i<=m;i++){
		st=find(t[i].u);
		ed=find(t[i].v);
		if(st!=ed){
			ans+=t[i].val;
			merge(st,ed);
			tot++;
			if(tot+1==n){
				printf("%lld",ans);
				return 0;
			}
		}
	}
	puts("-1");
}

时间复杂度\(O(m\log n+m\log m)\) 适用于稀疏图

upd 正确性证明

采用归纳法。证明每一步,都存在 mst 包含已选边集

算法刚开始时成立。

如果某一步成立,当前边集为 \(E\) ,属于 \(T\) 这棵 mst ,现在要加入 \(e\)

  • 如果 \(e\) 属于 \(T\) 则成立

  • 否则 \(T+e\) 中环上另一条不在 \(E\) 中的边 \(f\) (一定只有 1 条)

    1. \(f\) 的权值一定不小于 \(e\) 的权值,否则会先选择 \(f\)
    2. \(f\) 的权值一定不大于 \(e\) 的权值,否则 \(T+e-f\) 是一棵更小的生成树

    所以 \(f,e\) 权值相同,\(T+e-f\) 也是 mst ,且包含 \(E\)

Prim

Prim 适用于稠密图,思路接近 Dijkstra :每次选一个花费最小的点进行拓展

#include<bits/stdc++.h>
using namespace std;
const int N=5005;
int n,m,cnt,ans,g[N][N],vis[N],cost[N];
int main() {
	memset(g,100,sizeof(g));
	scanf("%d%d",&n,&m);
	for(int i=1,u,v,w;i<=m;i++) {
		scanf("%d%d%d",&u,&v,&w);
		if(g[u][v]>w)g[u][v]=w;
		g[v][u]=g[u][v];
	}
	for(int i=2;i<=n;i++)cost[i]=g[1][i];
	vis[1]=1;
	for(int C=1,mn,p;C<n;C++) {
		mn=2100000000,p=-1;
		for(int i=1;i<=n;i++)
		if(!vis[i] && cost[i]<mn)mn=cost[i],p=i;
		if(p==-1)return printf("Err"),0;
		vis[p]=1,ans+=mn;
		for(int i=1;i<=n;i++)
			if(!vis[i] && g[p][i]<cost[i])
				cost[i]=g[p][i];
	}
	printf("%d",ans);
} 

时间复杂度\(O(n^2)\)

关于堆优化

其实比较鸡肋:修改的次数太多了,使得复杂度没什么差别

upd Borůvka

古老的算法,在 \(O(m\log n)\) 的时间解决

  • 对于现在的每一个连通块,找到从这个连通块出发,不在 mst 中的,到达别的连通块的最短边。
  • 找完后,将这些边加入 mst

单次加入是 \(O(m)\) 的,会加入 \(\log n\) 次。

#include <bits/stdc++.h>
using namespace std;
const int maxn=5000+10, maxm=200000+10;
int n,m,fa[maxn],best[maxn],cnt,ans,used[maxm];
struct edge{int fr,to, w;}e[maxm];
int gf(int x){return fa[x]==x?fa[x]:fa[x]=gf(fa[x]);}
void init(int n){for(int i=1; i<=n; ++i) fa[i]=i;}
void mer(int a,int b){if(gf(a)!=gf(b)){fa[gf(b)]=gf(a);}}
int cmp(int a,int b){return e[a].w^e[b].w?e[a].w<e[b].w:a<b;}
void boruvka() {
    bool flg=true;
    while(flg){
        flg=false;
        memset(best,0,sizeof(best));
        for (int i=1;i<=m;i++) {
            if((gf(e[i].fr)!=gf(e[i].to))&&(!used[i])) {
                int fa=gf(e[i].fr),fb=gf(e[i].to);
                if (cmp(i,best[fa])) best[fa]=i;
                if (cmp(i,best[fb])) best[fb]=i;
            }
        }
        for(int i=1;i<=n;i++){
            if((best[i]!=0)&&(!used[best[i]])) {
                flg=true;cnt++,ans+=e[best[i]].w;
                used[best[i]]=true,mer(e[best[i]].fr,e[best[i]].to);
            }
        }
    }
}
int main() {
    scanf("%d%d",&n,&m); 
	init(n);
	e[0].w=0x7fffffff;
    for(int i=1;i<=m;i++) scanf("%d%d%d",&e[i].fr,&e[i].to,&e[i].w);
    boruvka();
    if(cnt==n-1) printf("%d",ans);
    else puts("orz");
    return 0;
}
posted @ 2021-01-26 16:16  小蒟蒻laf  阅读(532)  评论(0)    收藏  举报