大纲
-
Kruskal算法
-
Prim算法 and/or Dijkstra算法
最小生成树的概念
由一个点集V(表示所有点)和一个边集E(表示所有边)共同构成的数据结构。V和E甚至可以是空集。
延伸:
-
有向图、无向图
-
连通图、强连通图 连通图指所有点都相连;强连通图指在有向图中,每两个点都可以互相到达($ herefore强连通图subset连通图$)
-
有向无环图(DAG)、无向连通图
-
重边(两个点间有两条或以上的直连边)、自环(自己指向自己的边)
-
...
树?
无向无环连通图。
有根树,无根树
有根树中明确指定了两个点间的父子关系,所以可以认为边是有向的(由父亲指向儿子或相反) 无根树没有明确的父子关系,指定任意一个点当做根都可以。注意当选定作为根的顶点后,父子关系就明确了,无根树就变成了有根树。
生成树?
设图中有n个顶点,选出n-1条边来构成一个树,即称为该图的生成树。
最小生成树?
在某个图的所有生成树里边权之和最小的那个。
Kruskal算法
算法流程
初始化最小生成树中没有任何边。 然后由长度从小到大依次考虑每条边: 若加入最小生成树后形成了环,则不加入;否则加入。
分析
排序过程可以直接使用STL库的sort。 算法的瓶颈在于判断环上。
解法
使用并查集维护顶点之间的连接关系。 开始时没有任何点相连,即初始化并查集。 判断环时,只需判断该边的两个顶点是否原来就相连,即是否在一个集合中。 加边时,合并集合。
证明(来自OI wiki)
思路很简单,为了造出一棵最小生成树,我们从最小边权的边开始,按边权从小到大依次加入,如果某次加边产生了环,就扔掉这条边,直到加入了 $n-1$ 条边,即形成了一棵树。
证明:使用归纳法,证明任何时候 K 算法选择的边集都被某棵 MST 所包含。
基础:对于算法刚开始时,显然成立(最小生成树存在)。
归纳:假设某时刻成立,当前边集为 $F$ ,令 $T$ 为这棵 MST,考虑下一条加入的边 $e$ 。
如果 $e$ 属于 $T$ ,那么成立。
否则, $T+e$ 一定存在一个环,考虑这个环上不属于 $F$ 的另一条边 $f$ (一定只有一条)。
首先, $f$ 的权值一定不会比 $e$ 小,不然 $f$ 会在 $e$ 之前被选取。
然后, $f$ 的权值一定不会比 $e$ 大,不然 $T+e-f$ 就是一棵比 $T$ 还优的生成树了。
所以, $T+e-f$ 包含了 $F$ ,并且也是一棵最小生成树,归纳成立。
【代码】:
#include<bits/stdc++.h> using namespace std; int n,m,k; long long ans; const int maxn=500005; int fa[maxn]; struct edge //起点,终点,权值 { int x,y,z; }a[maxn]; int find(int x) //通过是否具有同一个父亲(是否在同一集合里)判断是否连通成环 { if(fa[x]!=x) fa[x]=find(fa[x]); return fa[x]; } bool cmp(edge x, edge y) //排序 { return x.z<y.z; } void kruskal() //核心算法 { for(int i=1;i<=n;i++) fa[i]=i; sort(a+1,a+m+1,cmp); for(int i=1;i<=m&&k<=n-1;i++) { int f1=find(a[i].x); int f2=find(a[i].y); if(f1!=f2) { ans+=a[i].z; fa[f1]=f2; //合并,连通起来 k++; } } if(k==n-1) cout<<ans<<endl; else cout<<"No Solution. "; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z); kruskal(); return 0; }
1.P3366 【模板】最小生成树
2.U69308 【常数PK系列】 #3 最小生成树
(WZ的题面描述啊不行的话是输出No Solution.)