通俗的说,一个图的最小生成树包含该图的所有顶点,且树中所有边的权值之和最小。若图中顶点数为n,则它的生成树含有n-1条边。对于生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边,则会形成一个回路。比较正式的定义:连通图的最小生成树是包含图中全部顶点的一个极小连通子图。
Prim算法思想:
假设
N
=
{
V
,
{
E
}
}
N=\{V,\{E\}\}
N={V,{E}}是连通网,
T
E
TE
TE是
N
N
N上最小生成树的边集合,Prim算法将图中所有顶点分为两类,一类为集合
U
U
U,该集合中的点表示该点已经被选取为生成树上的一个点,另一类为集合
V
−
U
V-U
V−U,表示还未被选取为生成树中的点,其中
V
V
V为图中所有点的集合。算法可以选取集合
V
V
V中的任意一个顶点初始化集合
U
U
U,即
U
=
{
u
0
}
(
u
0
∈
V
)
U=\{ u_0\}(u_0\in_\ V)
U={u0}(u0∈ V)。考虑集合
U
U
U与集合
V
−
U
V-U
V−U中相连的各条边,选取其中权值最小的那一条边
(
u
,
v
)
∈
E
,
u
∈
U
,
v
∈
V
−
U
(u,v)\in\ E,u\in_\ U,v\in\ V-U
(u,v)∈ E,u∈ U,v∈ V−U,其中
E
E
E为图中的边集,将点
v
v
v加入集合
U
U
U,边
(
u
0
,
v
0
)
(u_0,v_0)
(u0,v0)并入集合
T
E
TE
TE,重复上诉过程直至
U
=
V
U=V
U=V为止。此时
T
E
TE
TE中必有
n
−
1
n-1
n−1条边,则
T
=
(
V
,
{
T
E
}
)
T=(V,\{TE\})
T=(V,{TE})为
N
N
N的最小生成树。
代码实现
假设图中一共有V个顶点,图采用邻接矩阵的存储方式。设置两个辅助数组lowcost[V]和adjvex[V](大小都为V)。lowcost[i]存储了集合
U
U
U上所有点,到顶点
i
∈
V
−
U
i\in\ V-U
i∈ V−U的最小代价(权值),这样每次选取最小权值的边时,只需要选取lowcost数组中最小值元素代表的边即可!adjvex[i]存储了lowcost[i]代表的最小权值边的始点(从哪个点到达点i),假设lowcost[5]=10,adjvex[5]=1,则表示集合
U
U
U上的所有点,连接到5号顶点可能有多条边,其中权值最小的边为10,且权值最小的边的起始顶点存储在adjvex上,为1号顶点。
#include "iostream"
#include "vector"
#include "climits"
#include "stdio.h"
using namespace std;
const int MAX_VERTEX_NUM = 100;
const int INF = INT_MAX;
int N;//图中的顶点数
int lowcost[MAX_VERTEX_NUM], adjvex[MAX_VERTEX_NUM];
void MinSpanTree_PRIM(vector<vector<int> > &map, int u)
{
for (int i = 1; i <= N;i++){//初始化lowcost,和adjvex两个数据结构,集合U={u}开始
if(i==u)
{
lowcost[i] = 0;//表示开始开始点
adjvex[i] = 0;
}
else
{
lowcost[i] = map[u][i];
adjvex[i] = u;
}
}
for (int i = 1; i < N;i++)//循环N-1次,每次找到一个点加入集合U中
{
//以下代码为找到最小权值的边
int min = INT_MAX;
int index = -1;
for (int j = 1; j <= N;j++)
{
if(lowcost[j]!=0 && lowcost[j] < min)
{
min = lowcost[j];
index = j;
}
}
printf("最小生成树的边有:(%d - > %d), 权值为%d.\n", adjvex[index], index, min);
lowcost[index] = 0; //将该点加入集合U
//因为有新的点加入集合U,下面的for循环更新lowcost和adjvex两个数据结构
for (int m = 1; m <= N;m++)
{
if(m!=index && lowcost[m]!=0 && map[index][m] < lowcost[m])//考虑新加入的点index到U-V中各个点m的权值是否更小,是则更新
{
lowcost[m] = map[index][m];
adjvex[m] = index;
}
}
}
}
int main(){
int M;//边数
cin >> N >> M;
vector<vector<int> > map(N+1, vector<int>(N+1, INF));
for (int i = 1; i <= M;i++){
int u, v, w;
cin >> u >> v >> w;
map[u][v] = w;
map[v][u] = w;//无向图
}
MinSpanTree_PRIM(map, 1);
system("pause");
return 0;
}
输入数据如下,代表上图所示的图
6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 5 6
3 6 4
3 4 5
4 6 2
5 6 6
算法结果如下:
小结
Prim算法和dijkstra算法在算法思路步骤上很相似,都是由一个初始的集合开始生成树和找最短路,同时每次找到一个新的点加入集合后更新维护的数据结构。因此它们的代码结构也非常相似,外层一个大的for循环,每次经历一个循环表示找到一个点,循环内部有两个平行的for循环,内部的第一个for循环找最小边,内部的第二个for循环则是更新数据结构。所以该算法的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),
n
n
n为顶点个数,Prim算法时间复杂度与边数无关,适合于求边稠密的网的最小生成树。而克鲁斯卡尔算法恰恰相反,它的复杂度为
O
(
e
log
(
e
)
)
O(e\log(e))
O(elog(e)),e为网中边的数目,因此它相对于普里姆(Prim)算法而言,适合于求边稀疏的网的最小生成树。
参考资料:数据结构(C语言版).严蔚敏,吴伟民