无向图的最小生成树——Prim、Kruskal算法

最小生成树定义

G = ( V , E ) G = (V, E) G=(V,E) 是一个无向连通网,如果连通图的一个子图是一棵包含所有顶点的树(顶点数 = 边数 + 1),则该子图称为 G G G生成树(Spanning Tree)

  • 连接图中所有的 n n n 个点,并且只有 n − 1 n-1 n1 条边的子图就是它的生成树;
  • 生成树是连通图的包含图中的所有顶点的极小连通子图;
  • 图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树;
  • 只要能连通所有顶点而又不产生回路的任何子图都是它的生成树。

E E E 中每一条边 ( u , v ) (u, v) (u,v)上的权值 c ( u , v ) c(u, v) c(u,v),称为 ( u , v ) (u, v) (u,v) 的边长。图 G G G生成树上各边的权值(边长)之和称为该生成树的代价

在图 G G G 所有生成树中,代价最小的生成树称为最小生成树(Minimum-Cost Spanning Tree, MST)

  • 明确一点,最小生成树可能不是唯一的,但是最小生成树的权是唯一的。

最小生成树可以应用到许多实际问题。例如,在 n n n 个教室之间建局域网络,至少要架设 n − 1 n-1 n1 条通信线路,而每两个教室之间的距离可能不同,从而架设通信线路的造价就是是不一样的,那么如何设计才能使得总造价最小?

最小生成树的性质——贪心选择性证明

在这里插入图片描述

贪心选择性:假设 G = ( V , E ) G =(V, E) G=(V,E) 是一个连通网, U U U 是顶点集 V V V 的一个非空真子集。若 ( u , v ) (u,v) (u,v) 是一条具有最小权值(代价)的边,其中 u ∈ U , v ∈ V − U u∈U, v∈V-U uU,vVU,则必存在一棵包含边 ( u , v ) (u,v) (u,v) 的最小生成树。

此性质保证了 P r i m Prim Prim K r u s k a l Kruskal Kruskal 贪心算法的正确性。

MST贪心选择性的证明:(反证法)

【反证】设 G G G 的任何一棵最小生成树都不包含 ( u , v ) (u,v) (u,v) T T T 是连通网 G G G 上的一棵最小生成树。将边 ( u , v ) (u,v) (u,v) 加入到 T中,由生成树的定义,此时 T T T 中必包含一条 ( u , v ) (u,v) (u,v) 的回路:

  • 由于 T T T 是生成树,则在 T T T 上必存在另一条边 ( u ’ , v ’ ) (u’,v’) (u,v),且 u u u u ’ u’ u v v v v ’ v’ v 之间均有路径相通,即将边 ( u , v ) (u,v) (u,v) 加入到 T T T 中时,形成了环。

删去边 ( u ’ , v ’ ) (u’,v’) (u,v) 便可消去上述回路,得到生成树 T ′ T' T。但因为 ( u , v ) (u, v) (u,v) 的代价不高于 ( u ’ , v ’ ) (u’,v’) (u,v),所以 T ′ T' T 的代价亦不高于 T T T

  • 如果 c ( u , v ) = c ( u ’ , v ’ ) c(u, v)=c(u’,v’) c(u,v)=c(u,v),此时得到了另一棵最小生成树 T ′ T' T,与 “ G G G 的任何一棵最小生成树都不包含 ( u , v ) (u,v) (u,v)” 矛盾;
  • 如果 c ( u , v ) < c ( u ’ , v ’ ) c(u, v)<c(u’,v’) c(u,v)<c(u,v),此时得到的生成树 T ′ T' T 的代价小于 T T T 的代价,与 “ T T T 是连通网 G G G 上的一棵最小生成树” 矛盾;

性质得证。

Prim 算法

Prim 算法流程

无向连通图 G = ( V , E ) G=(V, E) G=(V,E),令 G G G 的最小生成树为 T = ( U , T E ) T=(U, TE) T=(U,TE)

算法过程:

  • 首先从顶点集 V V V 中任取一顶点(如顶点 v 1 v_1 v1)放入集合 U U U(已访问点集) 中。这时 U = { v 1 } , T E = { } U=\{v_1\} ,TE=\{ \} U={v1},TE={}
  • 然后找出权值最小的边 ( u , v ) (u,v) (u,v),且 u ∈ U , v ∈ ( V − U ) u∈U,v∈(V-U) uU,v(VU),将边加入 T E TE TE,并将顶点 v v v 加入集合 U U U
  • 重复上述操作直到 U = V U=V U=V 为止。这时 T E TE TE 中有 n − 1 n-1 n1 条边, T = ( U , T E ) T=(U,TE) T=(U,TE) 就是 G G G 的一棵最小生成树。

上面第二步中,如何找到连接 U U U V − U V-U VU 的最短边?

  • 类似于 Dijkstra 算法;
  • 利用 MST 性质,可以对于 V − U V-U VU 中的每个顶点,保存从该顶点到 U U U 中的各顶点的最短边;

在这里插入图片描述

Prim 算法实现

需要用到的数据结构:

  • 数组 L O W C O S T [ n ] LOWCOST[n] LOWCOST[n]:用来保存集合 V − U V-U VU 中各顶点与集合 U U U 中顶点最短边的权值;
    • L O W C O S T [ v ] = 0 LOWCOST[v]=0 LOWCOST[v]=0 表示顶点 v v v 已加入最小生成树中,也就是顶点 v v v 已经被访问过;
    • L O W C O S T [ v ] = i n f i n i t y LOWCOST[v]=infinity LOWCOST[v]=infinity 表示从顶点 v v v 无法到达当前已访问顶点集 U U U
  • 数组 C L O S S E T [ n ] CLOSSET[n] CLOSSET[n]:用来保存依附于该边的(集合 V − U V-U VU 中各顶点与集合 U U U 中顶点的最短边)在集合 U U U 中的顶点。

如何用数组 L O W C O S T [ n ] LOWCOST[n] LOWCOST[n] C L O S S E T [ n ] CLOSSET[n] CLOSSET[n] 表示候选最短边集? L O W C O S T [ i ] = w   &   C L O S S E T [ i ] = k LOWCOST[i]=w\ \&\ CLOSSET[i] = k LOWCOST[i]=w & CLOSSET[i]=k 表示顶点 v i , v k v_i,v_k vi,vk 之间的权值为 w w w,其中 v i ∈ V − U , v k ∈ U v_i \in V-U, v_k \in U viVU,vkU

如何更新这两个数组?

  • 当在 L O W C O S T [ n ] LOWCOST[n] LOWCOST[n] 数组中找到距离当前访问集 U U U 权值最小的边对应的顶点 v i v_i vi 之后,将 L O W C O S T [ i ] LOWCOST[i] LOWCOST[i] 置为 0,表示该点已经被访问;
  • 找到最小权值的边对应的顶点 v i v_i vi 之后,此时 v i ∈ U v_i \in U viU,判断 V − U V-U VU 中的顶点 v j v_j vj v i v_i vi 的距离( C [ i ] [ j ] C[i][j] C[i][j])与其到之前访问集的最小距离(也就是当前 L O W C O S T [ j ] LOWCOST[j] LOWCOST[j])的关系,如果 C [ i ] [ j ] < L O W C O S T [ j ] C[i][j]<LOWCOST[j] C[i][j]<LOWCOST[j],那么更新 L O E C O S T [ j ] = C [ i ] [ j ] , C L O S S E T [ j ] = i LOECOST[j]=C[i][j],CLOSSET[j]=i LOECOST[j]=C[i][j],CLOSSET[j]=i

实现步骤:

  • 初始化两个辅助数组 L O W C O S T LOWCOST LOWCOST C L O S S E T CLOSSET CLOSSET;;
  • 输出顶点 v s v_s vs ,将顶点 v s v_s vs 加入集合U中;
  • 重复执行下列操作 n − 1 n-1 n1
    • L O W C O S T LOWCOST LOWCOST 中选取最短边对应的 V − U V-U VU 中的顶点 v i v_i vi,取 C L O S S E T CLOSSET CLOSSET 中对应的顶点序号 k k k;
    • 输出顶点 k k k 和对应的权值;
    • 将顶点 k k k 加入集合U中;
    • 调整数组 L O W C O S T LOWCOST LOWCOST C L O S S E T CLOSSET CLOSSET;

下面给出没有做优化的 Prim 算法的伪代码:

void Prim(Costtype C[n+1][n+1], int s)  // 给定起点 s
{
    costtype LOWCOST[n+1]; int CLOSSET[n+1]; 
    int i, j, k; 
    costtype min;

    for(i=1; i<=n; i++)     //初始化数组LOWCOST和数组CLOSSET
    {
        LOWCOST[i] = C[s][i];
        CLOSSET[i] = s;
    }

    LOWCOST[s] = 0;     // 将起始点加入到最小生成树中
    
    for(i = 1; i <= n-1; i++ )
    {
        min = LOWCOST[1];
        k = 1;
        for(j = 1; j <= n; j++) {    // 在LOWCOST中选最短边,记 CLOSSET 中对应的顶点序号k
            if(LOWCOST[j] < min && LOWCOST[j] > 0)
            {
                min = LOWCOST[j];
                k=j; 
            }
        }
        cout << “(” << k << “,” << CLOSSET[k] << “)” << end1; // 输出最小生成树的边信息
        LOWCOST[k] = 0 ; // 把顶点k加入最小生成树中

        for (j = 1; j <= n; j++) {   // 调整数组 LOWCOST 和 CLOSSET
            if (C[k][j] < LOWCOST[j])
            {
                LOWCOST[j]=C[k][j];
                CLOSSET[j]=k;
            }
        }
    }
} /* 时间复杂度:O(|V|^2)

Prim 算法堆优化

使用最小堆进行优化,可以达到 O ( ∣ E ∣ l o g ∣ E ∣ ) O(|E|log|E|) O(ElogE) O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(ElogV) 的时间复杂度。不需要用到 L O W C O S T [ n ] LOWCOST[n] LOWCOST[n] C L O S S E T [ n ] CLOSSET[n] CLOSSET[n] 数组,但需要设置 v i s i t e d [ n ] visited[n] visited[n] 数组,表示哪些元素已经被加入到了最小生成树中。
(1)首先将与起点 s s s 相连的边加入到最小堆中,并置 v i s i t e d [ s ] = t r u e visited[s] = true visited[s]=true 表示顶点 s s s 已经加入到了最小生成树中;
(2)每一步从最小堆中弹出权值最小的边,该边有一个顶点 i i i 在当前的最小生成树中,另一个顶点 j j j 不在最小生成树中,置 v i s i t e d [ j ] = t r u e visited[j] = true visited[j]=true 表示将顶点 j j j 加入到了最小生成树中;
(3)判断所有与 j j j 邻接的边 ( j , k ) (j,k) (j,k),如果 v i s i t e d [ k ] = = f a l s e visited[k] == false visited[k]==false,那么将该边也加入到最小堆中;
(4)重复执行前面两个操作,直到所有节点都加入到最小生成树中时停止。

上面堆优化的 P r i m Prim Prim 算法的时间复杂度为 O ( ∣ E ∣ l o g ∣ E ∣ ) O(|E|log|E|) O(ElogE)

Kruskal 算法

Kruskal 算法流程

Kruskal 算法是一种巧妙利用并查集来求最小生成树的算法。

算法基本思想:

  • 设无向连通网为 G = ( V , E ) G=(V, E) G=(V,E),令 G G G 的最小生成树为 T = ( U , T E ) T=(U,TE) T=(U,TE),其初态为 U = V , T E = { } U=V,TE=\{\} U=V,TE={},即把每个顶点看成一个连通分量
  • 然后,按照边的权值由小到大的顺序,依次考察 G G G 的边集 E E E 中的各条边;
  • 若被考察的边连接的是两个不同连通分量,则将此边作为最小生成树的边加入到 T T T 中,同时把两个连通分量连接为一个连通分量;
  • 若被考察的边连接的是同一个连通分量,则舍去此边,以免造成回路;
  • 如此下去,当 T T T 中的连通分量个数为 1 时,此连通分量便为 G G G 的一棵最小生成树。

Kruskal 算法实现

实现步骤:

  • 初始化: U = V ; T E = { } U=V; TE=\{ \} U=V;TE={};
  • 循环直到 T T T 中的连通分量个数为 1:
    • 在E中选择最短边(u,v);
    • 如果顶点 u 、 v u、v uv 位于 T T T 的两个不同连通分量,则
      • 将边 ( u , v ) (u,v) (u,v) 并入 T E TE TE;
      • 将这两个连通分量合为一个;

判断两个点是否属于同一个连通分量,以及合并两个连通分量,这就能并查集的优势。

在这里插入图片描述

伪代码:

void Kruskal_Min_Tree(EdgeSet edges, int vexnum, int arcnum)
{ 
    int bnf, edf; int parents[100];
    Sort(edges);                        // 按照权值大小排序
    for(int i=0; i<vexnum; i++)         // 初始化parent[]数组
        parents[i]=i;

    for(i=0;i<arcnum;i++) {
        bnf = Find(edges[i].begin, parents);     // 并查集的 find() 函数
        edf = Find(edges[i].end, parents);

        if(bnf != edf) {
            parents[bnf]=edf;           // 连通分量合并,没做优化
            edges
            cout << ‘(’<<vertices[edges[i].begin].data<<‘’, ‘;
            cout << vertices[edges[i].end].data<<‘,’<<edges[i].cost<<‘) ’;
            cout << endl;
        }
    }
} /* 时间复杂度:O (|E|*log|E|)
  • 1
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值