11、数据结构与算法 - 基础:最小生成树

最小生成树

问题提出:

要在n个城市间建立通信联络网,城市间的通信线路造价不同,希望找到一种方案使得建立该通信网所需花费的总代价最小。

问题分析:

n个城市间,最多可设置n(n-1)/2条线路; n个城市间建立通信网,至少需n-1条线路;

问题转化为:如何在可能的线路中选择n-1条,能把所有城市(顶点)连起来,且总耗费(各边权值之和)** 最小**。

定义:

生成树中边的权值(代价)之和最小的树。

构造最小生成树的准则:

  • 必须只使用该网中的边来构造最小生成树;
  • 必须使用且仅使用n-1条边来联结网络中的n个顶点;
  • 不能使用产生回路的边。

实例:

 

普里姆(Prim)算法

普里姆方法的思想:

1)在图中任取一个顶点K作为开始点,令U={k},W=V-U,其中V为图中所有顶点集,

2)然后找一个顶点在U中,另一个顶点在W中的边中最短的一条,找到后,将该边作为最小生成树的树边保存起来,并将该边顶点全部加入U集合中,并从W中删去这些顶点,然后重新调整U中顶点到W中顶点的距离, 使之保持最小,

再重复此过程,直到W为空集止。求解过程参见下页图。

 

简单地说,此过程是不断地在W中找出与U里边相连的权值最小的边。

算法的基本思想

设置一个辅助数组T,对当前V-U集中的每个顶点,记录和顶点集U中顶点相连接的代价最小的边:

struct {
     int  adjvex;  // U集中的顶点序号
     int  lowcost;  // 边的权值
} T[MAX_VERTEX_NUM];

U为生成树上的顶点集,V-U为剩余顶点集,T[j]用于存储连接V-U中顶点j与U中顶点的权值最小的边及权值。

每次从数组T中取出权值最小的边加入到树中(用权值为0为来代表顶点已加入生成树集U)

1,初始化辅助数组,出发点加入U

2,对其余顶点  

2、 1 求出下一个加入树的顶点K ;

2、 2 输出加入树的边,将顶点K并入U ;

2、 3 修改其它顶点的最小边;

假设开始顶点就选为顶点1,故首先有U={1},W={2,3,4,5,6}

 

 

解释:第一个顶点1,它连接到3的权值最小为1,所以加入生成树中,并令T中1与3相连的边权值为0,同时加入与顶点3相连边的权值。不断重复此操作,直到w完毕。

 

算法实现:

void MiniSPanTree_PRIM(MGraph G, VertexType u)
{
    int k, j, i, minCost;
    k = LocateVex(&G, u);  //返回u在图G中的位置

    for(j=0; j<G.vexnum; ++j)  // 初始化辅助数组
    {
        if(j != k){
            T[j].adjvex = k;
            T[j].lowcost = G.A[k][j];
        }
    }
    T[k].lowcost = 0;

    for(i=1; i<G.vexnum; ++i)   /*G.vexnum-1次循环,寻找当前最小权值的边*/
    {
        minCost = MAXINF;
        for(j=0; j<G.vexnum; ++j)   /*求生成树的下一个顶点*/
        {
            if(T[j].lowcost < minCost && T[j].lowcost != 0){
                minCost = T[j].lowcost;
                k=j;
            }
        }
        printf("(%c, %c), %d\n", G.v[T[k].adjvex], G.v[k], T[k].lowcost);  /*输出生成树的边*/

        T[k].lowcost = 0; /*标记顶点k已加入生成树*/

        for(j=0;j<G.vexnum; ++j)  /*顶点k加入生成树后重新选择最小边*/
        {
            if(T[j].lowcost != 0 && G.A[k][j] <T[j].lowcost){
                T[j].adjvex = k;
                T[j].lowcost = G.A[k][j];
            }
        }
    }
}

时间复杂度:

在普里姆算法中,第一个进行初始化的for循环语句的执行次数为n-1,第二个for循环中又包括了两个for循环,执行次数为2(n-1)2 ,所以时间复杂度为O(n2)

克鲁斯卡尔(kruskal)算法

1.克鲁斯卡尔算法基本思想

克鲁斯卡尔算法的基本思想是:

将图中所有边按权值递增顺序排列,依次选定取权值较小的边,但要求后面选取的边不能与前面选取的边构成回路,

若构成回路,则放弃该条边,再去选后面权值较大的边,n个顶点的图中,选够n-1条边即可。

例如,对上图中无向网,用克鲁斯卡尔算法求最小生成树的过程见下图。

 

 

 

这一过程相当于选点加入树中。

kruskal算法的关键:如何判断加入的边是否形成回路。可以采用将各顶点划分为不同集合的方法来解决,每个集合中的顶点表示一个无回路的连通分量。

因此,设计一个数组int vset[MAXV],利用vset[i]来判定各顶点是否属于同一集合中。其初始值为vset[i]=i(i=0,1,...,MAXV-1),i为顶点所在集合的编号,表示各顶点在不同的集合上。

算法实现:

#define MAXE  //最大边数
# define MAXV  //最大顶点数

typedef struct
{
    int v1;
    int v2;    /*v1、v2是两 个顶点的序号*/
    int weight;  //边权
}Edge;  // 边集类型数据结构
void kruskal(Edge GE[], int n, int e)
{
    int i, j, m1, m2, s1, s2, k;
    int vset[MAXV];
    for(i=0; i<n;i++)  /* 初始化辅助数组 */
    {
        Vset[i] = i;
    }
    k = 0;  /*生成树中边的数目,初值为0*/
    j = 0;  /*边集数组GE的下标,初值为0*/
    while(k<=n-1)  /*生成树中的边数小于n条时继续循环*/
    {
        m1 = GE[j].v1;
        m2 = GE[j].v2;  /*从边集数组GE中取出一条边的两个顶点*/
        s1 = vset[m1];
        s2 = vset[m2];  /*分别得到两个顶点所在集合的编号*/
        if(s1 != s2)    /*两个顶点属于不同的集合,将该边加入最小生成树*/
        {
            printf("(%d, %d), %d", m1, m2, GE[j].weight);
            k++;    /*生成树的边数加1*/
            vset[m2] = s1;    /*使得两个集合编号相同*/
        }
        j++;  /*扫描下一条边*/
    }
}

假设,n为图中顶点个数,e为图中边的个数。在kruskal中,while循环是影响时间效率的主要操作,其循环次数最多为e次,所以克鲁斯卡尔算法的时间复杂度为O(e)。