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 条)
- \(f\) 的权值一定不小于 \(e\) 的权值,否则会先选择 \(f\)
- \(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;
}

浙公网安备 33010602011771号