1、最小生成树的定义
一个连通图的生成树是该连通图的一个极小连通子图,它含有图中全部顶点,但只构成一棵树的(n-1)条边。对于一个带权连通无向图G的不同生成树,各棵树的边上的权值之和可能不同,边上的权值之和最小的树称为该图的最小生成树。
2、最小生成树的应用
比如要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。
3、最小生成树的构造算法
图的邻接矩阵类型定义如下:
#define N 100
typedef char ElemType;
//adjacency matrix graph
typedef struct MGraph
{
ElemType vertexes[N];
int edges[N][N];
int n;
}MGraph;
(1)普里姆(Prim)算法
Prim算法的核心思想是,从一个顶点开始,加入顶点集合,将顶点集合中所有顶点到其他顶点的边作为候选边,每次从候选边中挑选最小权值的边作为生成树的边,然后更新候选边,直到图的所有顶点都被加入为止。其步骤如下:
①初始化顶点集合为传入的那个顶点,初始化候选边为该顶点到其他顶点的所有边
②重复步骤③,直到图中所有顶点都被加入为止
③从候选边中挑选最小权值的边作为生成树的边,此时因为加入了新的顶点,所以需要更新候选边
代码实现:
void Prim(MGraph& g, int k)
{
int* w = new int[g.n]; //the least edge weight
int* v = new int[g.n]; //vertexes of the least weight
for (int i = 0; i < g.n; i++)
{
w[i] = g.edges[k][i];
v[i] = k;
}
w[k] = 0;
for (int cnt = 1; cnt < g.n; cnt++)
{
//find the min val of w
int imin = -1;
for (int i = 0; i < g.n; i++)
{
if (w[i] != 0 && (imin == -1 || w[i] < w[imin]))
imin = i;
}
//print
cout << "(" << g.vertexes[v[imin]] << "," << g.vertexes[imin] << ")";
//update the least weight
w[imin] = 0;
for (int i = 0; i < g.n; i++)
{
if (g.edges[imin][i] != 0 && g.edges[imin][i] < w[i])
{
w[i] = g.edges[imin][i];
v[i] = imin;
}
}
}
delete[] w, v;
}
Prim算法包含两重for循环,其时间复杂度为O(n²)。
(2)克鲁斯卡尔(Kruscal)算法
Kruscal算法的基本思想是,将图的所有边按权值递增的顺序进行排序,然后每次依次从小到大选择一条边加入到生成树中,但加入的前提是加入这条边后不会构成回路。其步骤如下:
①将图的所有边按权值递增的顺序进行排序
②重复步骤③,直到图中所有顶点都被加入为止
③按照从小到大的顺序选择一条边,如果这条边未使生成树形成回路,就把这条边加入到生成树中
代码实现如下:
void Kruscal(MGraph& g)
{
//define the Edge
typedef struct Edge
{
int u, v;
int w;
Edge(int _u, int _v, int _w) :u(_u), v(_v), w(_w) {}
}Edge;
//init the edges
vector v;
for (int i = 0; i < g.n; i++)
{
for (int j = 0; j < g.n; j++)
{
auto w = g.edges[i][j];
if (w != INT16_MAX && w != 0)
v.push_back(Edge(i, j, w));
}
}
//sort by weight increment
sort(v.begin(), v.end(), [](Edge u, Edge v)->int {
return u.w < v.w;
});
//add all edges if it does not constitute a loop
int* set = new int[g.n];
for (int i = 0; i < g.n; i++)
set[i] = i;
vector::iterator iter = v.begin();
for (int cnt = 0; cnt < g.n - 1; )
{
if (set[iter->u] != set[iter->v])
{
cout << "(" << g.vertexes[iter->u] << "," << g.vertexes[iter->v] << ")";
cnt++;
for (int i = 0; i < g.n; i++)
{
if (set[i] == set[iter->v])
set[i] = set[iter->u];
}
}
iter++;
}
delete[] set;
}
Kruscal算法同样包含两重for循环,其时间复杂度为O(n²),但可以对Kruscal算法做两方面的优化,一是将边集排序改为堆排序,二是用并查集来判断新加入边是否构成回路,优化后Kruscal算法的时间复杂度为O(elog₂e),e为图的所有边。
4、测试
求出给定无向带权图的最小生成树。图的定点为字符型,权值为不超过100的整形。
第一行为图的顶点个数n 第二行为图的边的条数e 接着e行为依附于一条边的两个顶点和边上的权值
最小生成树中的边。
6 10 ABCDEF A B 6 A C 1 A D 5 B C 5 C D 5 B E 3 E C 6 C F 4 F D 2 E F 6
(A,C)(C,F)(F,D)(C,B)(B,E) (A,C)(D,F)(B,E)(C,F)(B,C)
代码如下:
#include
#include
参考文献
[1] 李春葆.数据结构教程.清华大学出版社,2013.
[2] 最小生成树.百度百科[引用日期2018-04-29].