資料結構和演算法學習筆記八:帶權連通圖的最小生成樹

movin2333 發表於 2021-07-05
演算法 資料結構

一.簡介:

  對於一個n個頂點的連通圖,其最小生成樹是指將所有頂點連線起來的權值之和的最小樹,樹中包含n個頂點和n-1條邊.最小生成樹常見的生成演算法有普里姆演算法和克魯斯卡爾演算法,它們分別基於頂點的角度和邊的角度生成最小生成樹.

  宣告:對於本文中實現圖結構的各種類,詳見:資料結構和演算法學習筆記六:圖的相關實現 - movin2333 - 部落格園 (cnblogs.com)

二.兩種演算法簡介

  1.普里姆演算法:普里姆演算法基於頂點實現,基本思路是將所有已經納入到最小生成樹中的頂點儲存起來,然後遍歷當前的最小生成樹的端點,找出權值最小且不會閉環的邊並延伸最小生成樹,然後將新的頂點納入到最小生成樹中(和其他已經納入到樹中的頂點一起儲存起來)

  2.克魯斯卡爾演算法:克魯斯卡爾演算法基於邊實現,首先將所有邊按照權值由小到大排序,然後再從小到達依次遍歷所有邊,一一判斷當前邊加入最小生成樹中後是否會形成環路,在不形成環路的情況下將此邊加入最小生成樹,並將頂點儲存起來.頂點的儲存結構類似於倒置的樹,根節點在最下方.在最小生成樹的生成過程中可能會同時存在多顆頂點樹,但是最終所有頂點樹會匯聚成一顆.

三.程式碼實現(c#)

/************************************
* 建立人:movin
* 建立時間:2021/7/4 19:55:02
* 版權所有:個人
***********************************/
using System;
using System.Collections.Generic;
using System.Text;

namespace GraphCore
{
    /// <summary>
    /// 最小生成樹演算法
    /// </summary>
    public class MinimumCostSpanningTreeUtil
    {
        /// <summary>
        /// 計算最小生成樹-普里姆演算法
        /// 要求引數必須是一個連通圖,此處沒有校驗引數graph是否是連通圖的過程,可自行新增
        /// </summary>
        /// <param name="graph"></param>
        /// <param name="findAEdgeCallBack">找到一條邊後的回撥函式,引數為邊的兩個關聯點下標和權值</param>
        public static void MiniSpanTree_Prim(AdjacencyMatrixGraph graph,Action<int,int,int> findAEdgeCallBack = null)
        {
            //陣列lowcast,陣列的長度和頂點的個數一致,陣列中每個下標的值和頂點一一對應
            //lowcast的作用有兩個,以lowcast[1] = 5為例,意思是當前已經找過的頂點中到1頂點的最短路徑權值為5
            //所以作用一是某下標對應值不為0時代表當前已經生成的部分最小生成樹到某下標對應頂點的權值最小的邊的權值
            //作用二是某下標對應值為0時代表此下標對應頂點已經在最小生成樹中,不再參與繼續生成最小生成樹
            int[] lowcast = new int[graph.Count];
            //陣列adjvex,這個陣列作用是對應記錄lowcast中最小權值邊的另一個依附頂點下標(一個依附頂點下標就是lowcast下標)
            int[] adjvex = new int[graph.Count];

            lowcast[0] = 0;//從0號頂點開始生成最小生成樹,首先將0號頂點對應位置置為0
            //adjvex[0] = 0;//這句程式碼加不加都ok,0號位已經加入最小生成樹,這個值也就用不上了
            //初始化lowcast陣列的其他下標值
            for(int i = 1;i < lowcast.Length; i++)
            {
                //當前最小生成樹中只有0號頂點,所以以0號頂點到i號頂點的邊的權值就是當前的最小邊權值
                lowcast[i] = graph.adjacencyMatrix[0, i];
                //這些邊的另一個依附頂點當然是0號頂點
                adjvex[i] = 0;
            }

            //開始計算最小生成樹,結果儲存到result中

            int min = int.MaxValue;//用來儲存找到的最小權值邊的權值的臨時變數
            int tempIndex = 0;//用來儲存即將加入最小生成樹的邊的頂點(也就是即將加入最小生成樹的頂點)的臨時變數,另一個頂點儲存在adjvex陣列中
            //迴圈length-1次,每次將一個頂點和一條邊加入最小生成樹中
            for(int i = 1;i < graph.Count; i++)
            {
                //迴圈在當前的lowcast中找到非0的最小值(到沒有找過的頂點中的最小邊)
                min = int.MaxValue;
                tempIndex = 0;
                for(int j = 1;j < lowcast.Length; j++)
                {
                    if(lowcast[j] != 0 && lowcast[j] < min)
                    {
                        min = lowcast[j];
                        tempIndex = j;
                    }
                }
                //找到邊後呼叫回撥函式
                if(findAEdgeCallBack != null)
                {
                    findAEdgeCallBack(tempIndex, adjvex[tempIndex], lowcast[tempIndex]);
                }
                //更新lowcast陣列
                lowcast[tempIndex] = 0;

                //每次延申了最小生成樹後需要將lowcast中的值更新,方便下次繼續延申最小生成樹
                //剛才將下標為tempIndex的頂點和一條邊加入了最小生成樹,接下來只需要更新這個頂點相關的邊即可
                for(int j = 1;j < lowcast.Length;j++)
                {
                    //判斷頂點tempIndex和頂點j之間的邊
                    //j頂點不在最小生成樹中且這條邊的權值比lowcast中記錄的最小權值要小時
                    //更新到頂點j的最小權值邊的權值,並且記錄到頂點j的最小權值邊的另一個頂點為tempIndex
                    if(lowcast[j] != 0 && lowcast[j] > graph.adjacencyMatrix[tempIndex, j])
                    {
                        lowcast[j] = graph.adjacencyMatrix[tempIndex, j];
                        adjvex[j] = tempIndex;
                    }
                }
            }

        }
        /// <summary>
        /// 計算最小生成樹-克魯斯卡爾演算法
        /// 要求引數必須是連通圖
        /// </summary>
        /// <param name="graph"></param>
        /// <param name="findAEdgeCallBack">找到一條邊後的回撥函式,引數為邊的兩個關聯點下標和權值</param>
        public static void MinSpanTree_Kruskal(EdgesetArrayGraph graph, Action<int, int, int> findAEdgeCallBack = null)
        {
            //將邊集陣列排序
            SortEdgeNode(graph.edgeNodes);
            //宣告一個陣列,陣列下標對應頂點下標
            //陣列中值為-1時代表對應頂點還沒有加入最小生成樹
            //當某個頂點被加入最小生成樹後,將陣列中對應的下標的值修改,修改後的值指向下一個加入最小生成樹的頂點下標
            //如vertices[5] = 7代表5號頂點和7號頂點都在最小生成樹中,其中5號頂點的下一個頂點是7號頂點
            //在構建最小生成樹的過程中會通過這個陣列檢驗當前邊新增進陣列是否會構成環
            //分析後面的程式碼可以知道,最終陣列中length-1個值會被修改,剛好對應新增到最小生成樹中的length-1條邊
            int[] vertices = new int[graph.edgeNodes.Length];
            //陣列初始值都為-1
            for (int i = 0; i < vertices.Length; i++)
            {
                vertices[i] = -1;
            }

            //下面構建最小生成樹

            //迴圈遍歷所有邊,一一校驗是否可以加入最小生成樹
            for (int i = 0; i < graph.edgeNodes.Length; i++)
            {
                EdgesetArrayEdgeNode node = graph.edgeNodes[i];
                int startIndex = GetNextVertex(vertices, node.headIndex);
                int endIndex = GetNextVertex(vertices, node.tailIndex);
                //檢驗是否成環,不成環則這條邊可以加入最小生成樹
                if (startIndex != endIndex)
                {
                    vertices[startIndex] = endIndex;
                    if(findAEdgeCallBack != null)
                    {
                        findAEdgeCallBack(node.headIndex, node.tailIndex, node.weight);
                    }
                }
            }
        }
        /// <summary>
        /// 在vertices中,頂點之間的先後次序最終的儲存方式類似於一顆倒過來的樹,根頂點在最下方,儲存時會一直向下找,直到找到根頂點,儲存時會將下一個儲存到最小生成樹中的頂點掛到根頂點下方成為新的根頂點
        /// 查詢時看此頂點是否有後繼頂點,如果有那麼繼續查詢後繼頂點的後繼頂點...以此類推,直到某個頂點對應下標值為-1,即沒有後繼頂點,返回這個頂點下標
        /// 如果兩個頂點之間會構成環路,那麼它們所在的頂點的後繼中一定會有相同的頂點,最終查詢下去得到的值為頂點相同
        /// </summary>
        /// <param name="vertices"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        private static int GetNextVertex(int[] vertices,int index)
        {
            while(vertices[index] != -1)
            {
                index = vertices[index];
            }
            return index;
        }
        /// <summary>
        /// 將給定邊集陣列按照從小到達排序
        /// 採用選擇排序
        /// </summary>
        /// <param name="graph"></param>
        private static void SortEdgeNode(EdgesetArrayEdgeNode[] edgeNodes)
        {
            for (int i = 0; i < edgeNodes.Length; i++)
            {
                int minIndex = i;
                for (int j = i + 1; j < edgeNodes.Length; j++)
                {
                    if(edgeNodes[minIndex].weight > edgeNodes[j].weight)
                    {
                        minIndex = j;
                    }
                }
                if(minIndex != i)
                {
                    EdgesetArrayEdgeNode temp = edgeNodes[i];
                    edgeNodes[i] = edgeNodes[minIndex];
                    edgeNodes[minIndex] = temp;
                }
            }
        }
    }
}