针对无向图,而且图必须联通。最小生成树是指保持图的联通性的基础上保留n-1条边,裁剪掉其它边,保证留下来的边的权值总和最小而且不可以存在环路(裁剪原则)。通常有Prim算法和Kruskal算法。其中Prim算法时间复杂度为O(n^2),适合稠密图;Kruskal算法时间复杂度为O(e*lne),与边的条数有关,适合稀疏图。当图的各个边权值不同时,最小生成树唯一
1、Prim算法
1).输入:一个加权连通图,其中顶点集合为V,边集合为E;
2).初始化:Vnew = {x},其中x为集合V中的任一节点(起点),Enew 为空;
3).重复下列操作,直到Vnew = V:
a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
b.将点v加入集合Vnew中,将边<u, v>边加入集合Enew中;
4).输出:使用集合Vnew和Enew来描述所得到的最小生成树。
#define MAX 1000 // 这个数表示无穷,大于所有边的权值即可
#define N 6 // 点的个数
int label[N]; // 标记数组,标记该点是否被加入到vnew中
// 实际操作过程中可以没有vnew这个数组
int lowcost[N]; // 表示点到vnew的最小权值
int adjecent[N]; // 表示对应最小权值的vnew中的点
int edge[N-1][3]; // 记录最终结果边
void prim(int start) // 指定起点
{
int i;
for(i=0; i<N; i++) // 初始化label lowcost adjecent
{
label[i] = 0; // 初始为未标记状态0
lowcost[i] = a[start][i]; // a为邻接矩阵
// a[x][y]为MAX当x==y或者xy之间不是邻接时
adjecent[i] = start;
}
label[start] = 1; // 标记起点
int j, min, u;
for(i=0; i<N-1; i++) // 循环标记N个点,起点已标,所以循环N-1
{
u = -1; // u min用来选取每次加入vnew的权值最小的点
min = MAX;
for(j=0; j<N; j++)
{
if(label[j] == 0 && lowcost[j] < min)
{
// 遍历所有点,当该点不在vnew中,而且改点权值较小
// 记录该点,最终得到最小的点
min = lowcost[j];
u = j;
}
}
if(u != -1)
{
// 找到符合条件的点, 记录选择的这条边
edge[i][0] = u;
edge[i][1] = adjecent[u];
edge[i][2] = a[u][adjecent[u]];
label[u] = 1; // 标记该点,加入vnew
for(j=0; j<N; j++)
if(label[j] == 0 && a[u][j] < lowcost[j])
{
// 加入u点之后更新lowcost adjecent
lowcost[j] = a[u][j];
adjecent[j] = u;
}
}
}
}
2、Kruskal算法
首先将图的n个顶点看成n个孤立的连通分支(n个孤立点)并将所有的边按权从小大排序。然后按照边权值递增顺序,如果加入边后存在环则这条边不加,直到形成连通图。(如果加入边的两个端点位于不同的连通支,那么这条边可以顺利加入而不会形成环)
#define E 10 // 定义边数
#define N 6 // 定义点数
int edge[E][3]={{起点,终点,权重},{后面省略}}; // 表示所有边
int label[N] = {0}; // 标记点是否在最小生成树中,0为不在
int result[N-1][3]; // 结果最小生成树边
void kruskal()
{
int i, j, tmp;
// 按从小到大排序edge,排序方法可任选
for(i=0; i<E-1; i++)
for(j=i+1; j<E; j++)
if(edge[i][2] > edge[j][2])
{
// 交换起点
tmp = edge[i][0];
edge[i][0] = edge[j][0];
edge[j][0] = tmp;
// 交换终点
tmp = edge[i][1];
edge[i][1] = edge[j][1];
edge[j][1] = tmp;
// 交换权重
tmp = edge[i][2];
edge[i][2] = edge[j][2];
edge[j][2] = tmp;
}
// 遍历edge,保留所有符合条件的边(无环)
int index = 0; // 记录result值时使用的变量
for(i=0; i<E; i++)
{
// 如果这条边的起点终点均被标记,将会产生环路,放弃该边
if(label[edge[i][0]] + label[edge[i][1]] == 2)
continue;
// 满足条件,则标记起点终点,并将该边加入到result中
label[edge[i][0]] = 1;
label[edge[i][1]] = 1;
result[index][0] = edge[i][0];
result[index][1] = edge[i][1];
result[index][2] = edge[i][2];
index ++;
}
}
3、对2的修正。(不删除2的原因以为前车之鉴)
2的逻辑有遗漏。主要遗漏位置为
// 如果这条边的起点终点均被标记,将会产生环路,放弃该边
if(label[edge[i][0]] + label[edge[i][1]] == 2)
continue;
这句话。本意是如果这条边的两个顶点如果都被标记那么就不选这条边,因为会产生环。后来发现这是不对的。比如0 1 2 3四个点。边<0,1>权值为1,边<2,3>权值为2,边<0,2>权值为4,边<1,3>权值为3。正确的最小生成树应该是<0,1><2,3><1,3>。而如果按照2的判断条件将会先标记0 1再标记2 3然后选择<1,3>这条边时由于1 3都被标记,这条边将会被遗漏。因此当图的点过多,那么可能遗漏的边就越多。将永远不会得到正确答案。
修正思路:最小生成树的中标记的点的个数在任何阶段总是大于选择的边数的。(除了开始一个点都没有标记,一条边都没有选择的情况,那时均为0)。如果它们出现了相等的情况,那么将产生环路。
改正后的代码:
int getLabelNum() //从标记数据中获取已经标记的点的个数
{
int i, count = 0;
for(i=0; i<N; i++)
{
if(label[i] == 1)
count++;
}
return count;
}
if(index!=0 && index==getLabelNum())//index表示边数并排除没边的情况
continue;