构造图的最小生成树
最小生成树:
构造最小生成树的方法:
构造最小生成树的方法多数利用了最小生成树的MST性质:
MST性质:设N =(V,E)是一个连通网,U是顶点集V上的一个非空子集,若边(u,v)是一条最小权值的边,其中u∈U,v∈U-V,则比存在一棵包含边(u,v)的最小生成树。
方法一:普里姆(Prim)算法
算法思想:
设N=(V,{E})是连通网,TE是N上最小生成树中边的集合
- 初始令U = {u₀},(u₀ ∈V),TE=Ф;
- 在所有u∈U,v∈V-U的边(u,v)∈E中,找到一条代价最小的边(u₀,v₀);
- 将(u₀,v₀)并入集合TE,同时v₀并入集合U中;
- 重复上述操作直至U==V为止,则T = (V,{TE})为N的最小生成树。
举例:
closedge数组:
附设一个辅助数组closedge,用以记录从V-U中的各个顶点到U的具有最小代价的边。
-
对于每一个顶点vi∈V-U,相应的分量为closedge[i-1],它应该包括两个域:
-
- lowcost储存该边上的权,即
-
closedge[i-1].lowcost = min{ cost ( u , vi ) | u ∈ U }
-
(网采用邻接矩阵表示法)
-
- Adjvex存储该边依附在U中的顶点
-
U中每增加一个顶点,只要考虑该新增顶点到vi这条边上的权值会不会更小即可。
closedge数组的组成部分: | - |
---|---|
lowcost | Adjvex |
储存该边上的权 | 存储该边依附在U中的顶点 |
closedge数组的应用举例:
(答案不唯一)
每次加入一个点,就看该点与其他点的关系,如果有比当前closedge数组中的权值更小的,就更新lowcost和adjvex,同时去掉原来的那条线。
算法实现:(图的组成结构参照数据结构之图论算法(一)图的存储结构及其构建算法中的邻接表表示)
//记录从顶点集U到V-U的代价最小的边的辅助closedge数组定义:
struct closedgetype{
VertexType adjvex; //存储最小权值边所连接的另一个点的地址
VRType lowcost; //存储边的最小权值
}closedge[MAX_VERTEX_NUM];
int GetWeight(Graph G, int start, int end){//start为起点地址,end为终点地址,该函数用于求两点间的距离;
if(start == end) return 0; //如果起点和终点为同一个,就返回0
adjVert *p = G.v[start].firstarc; //邻接表的探索的管用方法,先用p去指向邻接顶点相连的第一条边
while(p){
if(end == p->adjvert) return p->edgeInfo; //如果end==p所指的边连接的下一顶点,就说明这两点存在边,权值大小就是这条边的值
p = p->next; //如果不相等,就看下一条与start相连的边
}
//如果p==NULL了还没有return,说明start与end之间没有直接相连的边,则返回无穷大;
return INFINITY;
}
int MinCostVert(closedgetype closedge[], int sum){
int i, minvert = 0;
int min = INFINITY; //初始化
for(i = 0; i < sum; i++){
if(closedge[i].lowcost < min){
min = closedge[i].lowcost;
minvert = closedge[i].adjvex;
}
}
return minvert;
}
void MiniSpanTree_PRIM(Graph G, int u){
//用Prim算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边;
int k = u-1; //找到点u对应的顶点地址;
int sumcost = 0; //用于求总代价
int j, i;
//初始化:U = {V1},只有一个点
for(j = 0 ; j < G.n; j++){
closedge[j].adjvex = k; //表示每一个点如果都与u点相连的话,最小代价为多少
closedge[j].lowcost = GetWeight(G, k, j); //从u(起点地址)到j(终点地址)
}
for(i = 1; i < G.n; i++){
//从当前closedge中找到最小的邻接结点的地址k;
k = MinCostVert(closedge, G.n);
sumcost += closedge[k].lowcost;
//把k点并入集合U中:
closedge[k].lowcost = 0;
//重新选择最小边:
for(j = 0;j < G.n; j++){
if(GetWeight(G, i, j) < closedge[j].lowcost){
closedge[j].adjvex = i;
closedge[j].lowcost = GetWeight(G,i,j);
}
}
}
printf("%d\n", sumcost); //输出最小代价
}
如果图的构成是邻接矩阵的话,则代码可参考如下:
int MinCostVert(closedgetype closedge[], int sum){
int i, minvert = 0;
int min = INFINITY; //初始化
for(i = 0; i < sum; i++){
if(closedge[i].lowcost < min){
min = closedge[i].lowcost;
minvert = closedge[i].adjvex;
}
}
return minvert;
}
void MiniSpanTree_PRIM(Graph G, int u){
//用Prim算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边;
int k = u-1; //找到点u对应的顶点地址;
int sumcost = 0; //用于求总代价
int j, i;
//初始化:U = {V1},只有一个点
for(j = 0 ; j < G.n; j++){
if(j != k){
closedge[j].adjvex = k; //表示每一个点如果都与u点相连的话,最小代价为多少
closedge[j].lowcost = G.arcs[k][j].adj;
}
}
closedge[k].lowcost = 0;
for(i = 1; i < G.n; i++){
//从当前closedge中找到最小的邻接结点的地址k;
k = MinCostVert(closedge, G.n);
sumcost += closedge[k].lowcost;
//把k点并入集合U中:
closedge[k].lowcost = 0;
//重新选择最小边:
for(j = 0;j < G.n; j++){
if(G.arcs[k][j].adj < closedge[j].lowcost){
closedge[j].adjvex = i;
closedge[j].lowcost = G.arcs[k][j].adj;
}
}
}
printf("%d\n", sumcost); //输出最小代价
}
大体不变,主要是把GetWeight函数换成了二维数组可以表示的值。
方法二:克鲁斯卡尔(Kruscal)算法
算法思想:
设连通网N =(V,{E})
- 令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量。
- 在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入T中,否则舍去该边而选择下一条代价最小的边,依次类推,直到T中所有顶点都在同一联通分量上为止。
用伪代码可表示为:
把所有边排序,记第i小的边为e[i](1<=i<m)//m为总边数;
初始化MST为空;
初始化连通分量,让每个点自成一个独立的连通分量;
for(int i = 0; i < m; i++){
if(e[i].u 和 e[i].v 不在同一个连通分量里){
把边e[i]并入MST;
合并e[i].u 和 e[i].v 所在的连通分量;
}
}
在上述的伪代码中,关键在于连通分量的查询与合并:
- 需要知道任意两个点是否在一个连通分量中;
- 还需要合并这两个连通分量
使用并查集(Union-Find set)
如同Prim算法要借助closedge数组一样,Kruskal算法需要使用并查集。
可以把每个连通分量看成一个集合,该集合包含了连通分量中的所有点,这些点两两相通,而具体的连通方式无关紧要,类似于集合中的元素没有先后顺序之分,只有“属于”和“不属于”之分。
图的所有连通分量可以用若干个不相交的集合表示,而集合又通过树来表示。
例如:包含点{1,2,3,4,5,6}的图有3个连通分量{{1,2}, {3,4,5}, {6}},那么就需要用3棵树来表示。规定每棵树的根节点是这棵树所对应的集合的代表元。
代表元存储在 p[ v ] 数组(parent)中,即把结点 vi 的父节点存储在 p[ i ] 中,因此可以写出“查找x所在树的根节点”的递归程序:(其中若 vi 没有父节点了,则p[i] = vi 本身)
(以邻接表为例)
int Findroot(int x){
return p[x] == x ? x : Findroot(p[x]); //如果x没有父节点了,即p[x] == x,就返回x;
//否则继续递归寻找根节点
}
为了提高效率,就将遍历过的结点都改成树根的子节点;因此上述代码改进为:
int Findroot(int x){
return p[x] == x ? x : p[x] = Findroot(p[x]);
}
这样,Kruscal算法的完整代码如下:
int CountArcs(Graph G){
int sum = 0;
for(int i = 0; i <G.n; i++){
adjVert *p = G.v[i].firstarc;
while(p){
sum++;
p = p->next;
}
}
return sum;
}
void setvalue_rank(Graph G,int *r, int *w, int *u, int *v){
int e = 0;
for(int i = 0; i < G.n; i++){
adjVert *arcp = G.v[i].firstarc;
while(arcp){
w[e] = arcp->edgeInfo;
u[e] = i;
v[e] = arcp->adjvert;
arcp = arcp->next;
e++;
}
}
for(int i = 0; i <= e-1; i++){
for(int j = i+1; j<= e; j++){
if(w[i] > w[j]){
r[i] = j;
r[j] = i;
}
}
}
}
int Findroot(int x, int *p){
return p[x] == x ? x : p[x] = Findroot(p[x], p);
}
//第i条边的两个端点分别储存在u[i],v[i]中,其权值储存在w[i]中;
int Kruscal(Graph G){
int m = CountArcs(G), ans = 0;
int p[MAX_VERTEX_NUM], *w, *r, *u, *v;
int i, j, e;
w = (int*)malloc(m * sizeof(int));r = (int*)malloc(m * sizeof(int));
u = (int*)malloc(m * sizeof(int));v = (int*)malloc(m * sizeof(int));
//初始化:
for(i = 0; i < m; i++) r[i] = i;
for(i = 0; i < G.n;i++) p[i] = i;
//赋值并排序
setvalue_rank(G,r,w,u,v);
for(i = 0; i < m; i++){
e = r[i]; int x = Findroot(u[e], p); int y = Findroot(v[e],p);
if(x != y){ans+=w[e]; p[x] = y;}
}
return ans;
}