10分鐘學會最小生成樹(Prim+Kruskal)

問題引入

暢通工程

​ 若要將n個城市之間原有的公路改造爲高速公路,這些城市之間原有公路網如右圖所示,每條邊上的數字表示高速公路的改造成本(單位:10億元)。如何以最低的成本來構建高速公路網,使得任意兩個城市之間都有高速公路相連?

在這裏插入圖片描述

算法概述

最小生成樹

Minimal Spanning Trees (MST)

​ 任何只由圖G的邊構成,幷包含G的所有頂點的樹稱爲G的生成樹

​ 加權無向圖G的生成樹的權重是該生成樹的所有邊的權重之和

最小生成樹是其所有生成樹中權重最小的生成樹

N個頂點,選取N-1條邊,構建一個連通圖,且這N-1條邊的權重之和最小

分析:很明顯,最小生成樹就是我們引入問題的解。那又該怎麼來構建最小生成樹呢

現在我們來介紹兩種構建最小生成樹的算法,Prim算法和Kruskal算法。

Prim

​ 普里姆算法,於1930年由捷克數學家沃伊捷赫.亞爾尼克(Vojtěch Jarník)首次發現,在1957年由美國計算機科學家羅伯特.C**.普里姆**(Robert C. Prim)獨立發現,1959年,傑出的荷蘭計算機科學家艾茲格.W.迪傑斯特拉(Edsger W. Dijkstra)再次發現了該算法,又被稱爲亞爾尼克算法普里姆-亞爾尼克算法具有貪心選擇性質 --> 貪心算法經典實例

實例分析:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8Ar1OwYl-1588988229068)(C:\Users\ASUS\Desktop\算法\最小生成樹\images\image-20200509074401208.png)]

設計思路

(1) 任意選定一點s,設集合S={s}

(2) 從**不在集合S的點中選出一個點j使得其與S內的某點i的距離最短**,則(i,j)就是生成樹上的一條邊,同時將j點加入S

(3) 轉到(2)繼續進行,直至所有點都己加入S集合

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-NOcVBInz-1588988229070)(C:\Users\ASUS\Desktop\算法\最小生成樹\images\image-20200509074602460.png)]

**小結:**Prim算法,以貪點爲核心,目的將n個點放進集合s{},適用於稠密圖(點少邊多)

那需要定義哪些數據結構來記錄一個頂點的信息?

int G[MAXN] [MAXN];
//存儲圖,這裏用的是整形,實際應用時可能會複雜一點,需要自定義數據類型

int closeset[n], //記錄不在S中的頂點i在S中的最近鄰接點closeset[i]

int lowcost[n], //記錄不在S中的頂點i到S的最短距離,即到最近鄰接點的權值 

int  used[n]; //標記頂點是否被訪問,訪問過的頂點標記爲1 

初始化

int n, m; //n存儲頂點的數量,m存儲邊的數量
int G[MAXN][MAXN];  //存儲圖

void init(){
    for(int i = 0 ; i < n ; i++){  
       for(int j = 0 ; j < n ; j++)  
           G[i][j] = INF;    //初始化圖中任兩點間距離爲無窮大 
    }
}

貪心選擇

void prim(){
    int closeset[n], //記錄不在S中的頂點在S中的最近鄰接點
    lowcost[n], //記錄不在S中的頂點到S的最短距離,即到最近鄰接點的權值 
    used[n]; //標記頂點是否被訪問,訪問過的頂點標記爲1 
     
    for (int i = 0; i < n; i++)
    {
        //初始化,S中只有第1個點(0)
        lowcost[i] = G[0][i]; 
        //獲取其他頂點到第1個點(0)的距離,不直接相鄰的頂點距離爲無窮大 
        
        closeset[i] = 0; 
        //初始情況下所有點的最近鄰接點都爲第1個點(0) 
        
        used[i] = 0; 
        //初始情況下所有點都沒有被訪問過
    }
    used[0] = 1;  //訪問第1個點(0),將第1個點加到S中
    
         //每一次循環找出一個到S距離最近的頂點 
     for (int i = 1; i < n; i++)
     {
         int j = 0;         
         /*每一次循環計算所有沒有使用的頂點到當前S的距離,
         得到在沒有使用的頂點中到S的最短距離以及頂點號 */
         for (int k = 0; k < n; k++)//核心:貪一個離s{}最近的點(找點)
             if ((!used[k]) && (lowcost[k] < lowcost[j])) 
                 j = k; //因爲j初始爲0(s的第一個點),lowcost[0]無窮大
         //如果頂點k沒有被使用,且到S的距離小於j到S的距離,將k賦給j  
         
         printf("%d %d %d\n",closeset[j] + 1, j + 1, lowcost[j]); 
         //輸出S中與j最近鄰點,j,以及它們之間的距離   
         used[j] = 1; //將j增加到S中    
         
         /*每一次循環用於在j加入S後,重新計算不在S中的頂點到S的距離,
         修改與j相鄰的邊到S的距離,即更新lowcost和closeset */
         for (int k = 0; k < n; k++)
         {
             if ((!used[k]) && (G[j][k] < lowcost[k])) 
                 //鬆弛操作,如果k沒有被使用,且k到j的距離比原來k到S的距離小 
             { 
                     lowcost[k] = G[j][k]; 
                 //將k到j的距離作爲新的k到S之間的距離 
                     closeset[k] = j; 
                 //將j作爲k在S中的最近鄰點 
             }
         } 
     }  
} 

時間複雜度分析:

T(n)=O(V^2) 【鄰接矩陣】–> 稠密圖

堆優化(小根堆):T(n) = O(VlogV)+O(ElogV) = O(ElogV)

堆優化(斐波那契堆):T(n) = O(E+VlogV)

小結

Prim算法是一種基**於貪心思想求解加權無向圖的最小生成樹**的算法

Prim算法的時間複雜度爲T(n)= O(V^2),適合於處理稠密圖

Kurskal

並查集 + 最小生成樹 --> Kruskal算法

克魯斯卡爾算法

一種用來查找最小生成樹的算法

由Joseph Kruskal於1956年發表【Joseph Bernard Kruskal,1928年1月29日-2010年9月19日,美國數學家、統計學家、計算機科學家、心理測量學專家】

設計思路

(1) 將邊按權值從小到大排序後逐個判斷,如果當前的邊加入以後不會產生環,那麼就把當前邊作爲生成樹的一條邊

(2) 一共選取(V-1)條邊(V爲頂點數),最終得到的結果就是最小生成樹

數據結構:並查集

數據結構定義和初始化

/* 定義邊(x,y),權爲w */
struct edge
{
    int x, y;
    int w;
};

edge e[MAX * MAX];
int rank[MAX];/* rank[x]表示x的秩 */
int father[MAX];/* father[x]表示x的父結點 */
int sum; /*存儲最小生成樹的總權重 */ 
 
/* 比較函數,按權值非降序排序 */
bool cmp(const edge a, const edge b)
{
     return a.w < b.w;
}

並查集

/* 初始化集合 */
void make_set(int x)
{
	father[x] = x;
	rank[x] = 0;
}

/* 查找x元素所在的集合,回溯時壓縮路徑 */
int find_set(int x)
{
	if (x != father[x])
	{
		father[x] = find_set(father[x]);
	}
	return father[x];
}
/* 合併x,y所在的集合 */
int union_set(int x, int y, int w)
{
    x = find_set(x);
    y = find_set(y);
	if (x == y) return 0;//不合並返回0
	if (rank[x] > rank[y])
	{
		father[y] = x;
	}
	else
	{
		if (rank[x] == rank[y])
		{
			rank[y]++;
		}
		father[x] = y;
	}
	sum += w; //記錄權重
	return 1;//合併返回1, 標記出邊(x,y)是否可以加入生成樹中
}


時間複雜度分析

邊排序所需時間:T(n) = O(ElogE)

Kruskal算法的實現通常使用並查集來快速判斷兩個頂點是否屬於同一個集合。**最壞的情況可能要枚舉完所有的邊,此時要循環|E|次,**所以這一步的時間複雜度爲O(Eα(V))【採用路徑壓縮後,每一次查詢所用的時間複雜度爲增長極爲緩慢的Ackerman函數的反函數——α(x) ,其增長非常慢,可以視爲常數

T(n)= O(Eα(V)) + O(ElogE) = O(ElogE) -->稀疏圖

例題HDU-1301

題目描述

The Head Elder of the tropical island of Lagrishan has a problem. A burst of foreign aid money was spent on extra roads between villages some years ago. But the jungle overtakes roads relentlessly, so the large road network is too expensive to maintain. The Council of Elders must choose to stop maintaining some roads. The map above on the left shows all the roads in use now and the cost in aacms per month to maintain them. Of course there needs to be some way to get between all the villages on maintained roads, even if the route is not as short as before. The Chief Elder would like to tell the Council of Elders what would be the smallest amount they could spend in aacms per month to maintain roads that would connect all the villages. The villages are labeled A through I in the maps above. The map on the right shows the roads that could be maintained most cheaply, for 216 aacms per month. Your task is to write a program that will solve such problems.

The input consists of one to 100 data sets, followed by a final line containing only 0. Each data set starts with a line containing only a number n, which is the number of villages, 1 < n < 27, and the villages are labeled with the first n letters of the alphabet, capitalized. Each data set is completed with n-1 lines that start with village labels in alphabetical order. There is no line for the last village. Each line for a village starts with the village label followed by a number, k, of roads from this village to villages with labels later in the alphabet. If k is greater than 0, the line continues with data for each of the k roads. The data for each road is the village label for the other end of the road followed by the monthly maintenance cost in aacms for the road. Maintenance costs will be positive integers less than 100. All data fields in the row are separated by single blanks. The road network will always allow travel between all the villages. The network will never have more than 75 roads. No village will have more than 15 roads going to other villages (before or after in the alphabet). In the sample input below, the first data set goes with the map above.

The output is one integer per line for each data set: the minimum cost in aacms per month to maintain a road system that connect all the villages. Caution: A brute force solution that examines every possible set of roads will not finish within the one minute time limit.

翻譯:熱帶島嶼拉格里山的首長有個問題。幾年前,大量的外援花在了村莊之間的額外道路上。但是叢林不斷地超越道路,因此龐大的道路網太昂貴而無法維護。老年人理事會必須選擇停止維護一些道路。左上方的地圖顯示了目前正在使用的所有道路,以及每月維護這些道路的費用。當然,即使路線不像以前那麼短,也需要採取某種方式在所有村莊之間保持通行。長老院長想告訴長老委員會每月要花多少錢才能維持連接所有村莊的道路。在上面的地圖中,這些村莊被標記爲A到I。右邊的地圖顯示了可以最便宜地維護的道路,每月可節省216英畝。您的任務是編寫一個解決此類問題的程序。

輸入由1到100個數據集組成,後面是僅包含0的最後一行。每個數據集都從僅包含數字n的行開始,n是村莊的數目,1 <n <27,並標記了村莊字母的前n個字母大寫。每個數據集都以n-1行完成,這些行以字母順序的村莊標籤開頭。最後一個村莊沒有電話。村莊的每條線均以村莊標籤開頭,後跟從該村莊到帶有字母標籤的村莊的道路的數量k。如果k大於0,則該行以k條道路中的每條道路的數據繼續。每條道路的數據是道路另一端的村莊標籤,其後是道路的每月維護成本(以acms爲單位)。維護成本將爲小於100的正整數。該行中的所有數據字段均由單個空格分隔。公路網將始終允許所有村莊之間的旅行。該網絡永遠不會超過75條道路。到其他村莊的村莊中,沒有一條道路會超過15條(在字母表中的前後)。在下面的示例輸入中,第一個數據集與上面的地圖一起顯示。

每個數據集的輸出爲每行一個整數:維護連接所有村莊的道路系統的每月最低費用(以aacms計)。警告:檢查每條可能的道路的暴力解決方案都不會在一分鐘的時間內完成。

輸入

9
A 2 B 12 I 25
B 3 C 10 H 40 I 8
C 2 D 18 G 55
D 1 E 44
E 2 F 60 G 38
F 0
G 1 H 35
H 1 I 35
3
A 2 B 10 C 40
B 1 C 20
0

輸出

216
30

上代碼,這是Kruskal算法的。

#include "bits/stdc++.h"
using namespace std;
const int maxx = 105;
int father[maxx];
int ran[maxx];
int ans = 0;
struct node
{
    int x, y;
    int v;
} qq[30];
inline bool cmp(node a, node b)
{
    return a.v < b.v;
}
 
inline void make_set(int x)
{
    father[x] = x;
    ran[x] = 0;
}
inline int find(int x)
{ //壓縮路徑的查找
    if (father[x] == x)
    { //如果自己的父節點是自己,那麼x 結點就是根結點
        return x;
    }
    return find(father[x]); // 返回父結點的根結點
}
inline int merge(int x, int y, int v)//按秩合併
{
    x = find(x);
    y = find(y);
    if (x == y)
        return 0;
    ans += v;
    if (ran[x] > ran[y])
    {
        father[y] = x;
    }
    else
    {
        father[x] = y;
        if (ran[x] == ran[y])
        {
            ran[y]++;
        }
    }
    return 1;
}
 
int main()
{
    int n, m;
    while (cin >> n && n)
    {
        ans = 0;
        m = 0;
        for (int i = 0; i <= n; i++)
        {
            make_set(i);
        }
        int x, y;
        int v;
        char a, b;
 
        for (int i = 0; i < n - 1; i++)
        {
            cin >> a >> x;
            for (int j = 0; j < x; j++)
            {
                cin >> b >> v;
 
                qq[j + m].x = a - 'A';
                qq[j + m].y = b - 'A';
                qq[j + m].v = v;
                /* code */
            }
            m += x;
        }
        sort(qq, qq + m, cmp);
        int s = 0;
        for (int i = 0; i < m; i++)
        {
            if (merge(qq[i].x, qq[i].y, qq[i].v))
            {
                s++;
            }
            if (s > n - 1)
            {
                break;
            }
        }
        cout << ans << endl;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章