最小生成树(Minimum Spanning Tree, MST)是在一个给定的无向图G(V,E)中求一棵树T,使得这棵树拥有图G中的所有顶点,且所有边都是来自图G中的边,并且满足整棵树的边权之和最小。
最小生成树的3个性质:
(1)最小生成树是树,因此其边数等于顶点数减1,且树内一定不会有环。
(2)对给定的图G(V, E),其最小生成树可以不唯一,但其边权之和一定是唯一的。
(3)由于最小生成树是在无向图上生成的,因此其根节点可以是在这棵树上的任意一个结点。
求解最小生成树一般有两种算法——prim算法和kruskal算法。
prim算法基本思想:
(1)对图G(V, E)设置集合S,存放已被访问的顶点,然后每次从集合V-S中选择与集合S的最短距离最小的一个顶点(记为u),访问并加入集合S,同时令把这条离集合S最近的边加入最小生成树中。
(2)令顶点u为中介点,优化所有从u能到达的顶点v与集合S之间的最短距离。这样操作执行n次(n为顶点个数),直到集合S已包含所有顶点。可以发现,prime算法的思想与最短路径中Dijkstra算法的思想几乎完全相同,只是在设计最短距离时使用了集合S代替Dijkstra算法中的起点s。
prime算法的具体实现:
(1)集合S的实现方法和DIjkstra中相同,即使用一个bool型数组vis[]表示顶点是否被访问,其中vis[i] = false表示顶点Vi未被访问。
(2)不妨令int型数组d[]来存放顶点Vi(0<= i <= n-1)与集合S的最短距离,初始时除了起点s的d[s]赋为0,其余顶点都赋为一个很大的数表示INF,即不可达。
伪代码如下:
//G为图,一般设为全局变量:数组d为顶点与集合S的最短距离
Prim(G, d[]){
初始化;
for(循环n次){
u = 使d[u]最小的还未被访问的顶点的标号;
记u已被访问;
for(从u出发能到达的所有顶点v){
if(v未被访问 && 以u为中介点使得v与集合S的最短距离d[v]更优){
将G[u][v]赋值给v与集合S的最短距离d[v];
}
}
}
}
Dijkstra算法和prime算法只有优化d[v]的部分不同,而其他语句都是相同的。
邻接矩阵版prim
constint MAXV = 1000; //最大顶点数
constint INF = 1000000000; //设INF为一个很大的数
int n, G[MAXV][MAXV]; //n为顶点数,MAXV为最大顶点数
int d[MAXV]; //顶点与集合S的最短距离
bool vis[MAXV] = {false}; //初值为false,未访问
intprim( ){ //默认0号为初始点,函数返回最小生成树的边权之和
fill(d, d+MAXV, INF); //fill函数将整个数组赋为INF
d[0] = 0; //只有0号顶点到集合S的距离为0,其余为INF
int ans = 0; //存放最小生成树的边权之和
for(int i=0; i
邻接表版prim:
struct Node{
int v, dis; //v为边的目标顶点,dis为边权
};
vector Adj[MAXV]; //图G,Adj[u]存放从顶点u出发可以到达的所有顶点
int n; //
int d[MAXV];
bool inq[MAXV] = {false};
intprime(){
fill(d, d+MAXV, INF);
d[0] = 0;
int ans =0;
for(int i=0; i
和prim算法不同,kruskal算法采用了边贪心的策略,其基本思想为:在初始状态是隐去图中的所有边,这样图中每个顶点都自成一个连通块,之后执行下面的步骤:
(1)对所有边按边权从小到大进行排序。
(2)按边权从小到大测试所有边,如果当前测试边所连接的两个顶点不在同一个连通块中,则把这条测试边加入当前最小生成树中;否则,将边舍弃。
(3)执行步骤(2),直到最小生成树中的边数等于总顶点数减1或是测试完所有边时结束。而当结束时如果最小生成树的边数小于总顶点数减1, 说明该图不连通。
简单来说就是:每次选择图中最小边权的边,如果边两端的顶点在不同的连通块中,就把这条边加入最小生成树中。
对kruskal来说,由于需要判断边的两个端点是否在不同的连通块中,因此边的两个端点一定是需要的:而算法中又涉及边权,因此边权也必须要有。
定义边的结构体如下:
struct edge{
int u, v; //边的两个端点编号
int cost; //边权
}E[MAXV]; //最多有MAXV条边
在解决边的定义后,需要写一个排序函数来让 数组E按边权从小到大排序,因此不妨自定义sort的cmp函数:
bool cmp(edge a, edge b){
return a.cost < b.cost;
}
kruskal伪代码如下:
int kuskal(){
令最小生成树的边权之和为ans,最小生成树的当前边数为Num_Edge;
将所有边权从小到大排序;
for(从小到大枚举所有边){
if(当前测试边的两个端点在不同的连通块中){
将该测试边加入最小生成树中;
ans+= 测试边的边权;
最小生成树的当前边数Num_Edge加1;
当边数Num_Edge等于顶点数减1时结束循环;
}
}
return ans;
}
对于(1)如何判断测试边的两个端点在一个连通块中,(2)如何将测试边加入到连通块中,就需要用到并查集的知识,并查集可以通过查询两个结点所在集合的根节点是否相同来判断它们是否在同一个集合,然后通过合并功能可以将测试边的两个端点加入到集合合并,就能到达将边加入最小生成树的效果。
并查集部分代码如下:
//并查集部分
int father[MAXV]; //并查集数组
int findFather(int x){//并查集查询函数
int a = x;
while(x != father[x]) {//初始化
x = father[x];
}
//路径压缩
while(a != father[a]){
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
kruskal算法完整代码如下:
#include
#include
using namespace std;
const int MAXV = 110;
const int MAXE = 10010;
//边集定义部分
struct edge{
int u, v; //边的两个端点编号
int cost; //边权
}E[MAXV]; //最多有MAXV条边
bool cmp(edge a, edge b){
return a.cost < b.cost;
}
//并查集部分
int father[MAXV]; //并查集数组
int findFather(int x){//并查集查询函数
int a = x;
while(x != father[x]) {//初始化
x = father[x];
}
//路径压缩
while(a != father[a]){
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
//kruskal部分,返回最小生成树的边权之和,参数n为顶点个数,m为图的边数
int kruskal(int n, int m){
//ans为所求边权之和,Num_Edge为当前生成树的边数
int ans = 0, Num_Edge = 0;
for(int i=0; i