最小生成樹問題的兩種算法

超級詳細的基礎算法和數據結構合集:
https://blog.csdn.net/GD_ONE/article/details/104061907

摘要

本文主要介紹最小生成樹以及求最小生成樹常用的兩種算法,Prim算法和Kruskal算法。

最小生成樹的定義

最小生成樹是一個圖的總邊權最小的極小連通子圖

Prim算法

Prim算法和Dijkstra算法實現方式十分相似
Dijkstra是先設定一個點集,初始時點集爲空,每次找出一個距離起點最近的點,將該點加入集合,然後短縮其該點的鄰接點到頂點的距離。
Prim是先設定一個邊集,初始時邊集爲空,每次從待查找的邊中找出一個與邊集中的點相連的最短邊,然後將該邊加入邊集,然後將該邊的鄰接邊的狀態更新爲待查找。
另:Prim適用於稠密圖

圖示:
初始時所有邊都未加入邊集

在這裏插入圖片描述
從一號點開始,將其鄰接點的狀態設爲待查找

在這裏插入圖片描述
然後找出待查找的邊中的最短邊並將其鄰接邊的狀態設爲待查找

在這裏插入圖片描述
重複以上兩步:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
所有點都被加入最小生成樹,算法結束,最小生成樹爲:
在這裏插入圖片描述
接下來就是代碼實現了
對於實現Prim算法,需要兩個數組, 一個標記該點是否在邊集內,一個存儲該點到邊集的距離(也就是標記狀態的數組),還需要一個整型變量存儲最小生成樹的總邊權。

第一次循環只能更新1號點和其鄰接點的距離,此時還沒有一條邊加入最小生成樹,接下來每循環1次,能得到一個最短邊,所以要循環n次。
先看下代碼:

public static int Prim(){
        Arrays.fill(dis, 0x3f3f3f3f); //設所有邊距離邊集的的距離爲正無窮,也就是設所有邊的狀態爲不可查找
        
        int res = 0;// 存儲最小生成樹的總邊權
        dis[1] = 0; // 設1號點距離邊集的距離爲0
        for(int i = 0; i < n; i++){// 循環n次
            int t = -1;    
            int minv = INF; //存儲最短邊
            
            for(int j = 1; j <= n; j++){
                if(st[j] == 0 && minv > dis[j]){
                    t = j;
                    minv = dis[j];
                }
            }
            
            if(minv == INF) return INF;// 最短邊是無窮大,說明該圖不連通
            res += dis[t];
            st[t] = 1;
           //將t的鄰接邊的狀態更新爲待查找
            for(int j = 1; j <= n; j++){
                // 或者說是縮短t的鄰接點到達邊集的距離    
                dis[j] = Math.min(dis[j], g[t][j]); 
            }
            
        }
        return res;
    }

例題:Prim算法求最小生成樹
代碼:

import java.io.*;
import java.util.*;


public class Main{
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
    
    static final int N = 1005, INF = 0x3f3f3f3f; 
    static int[][] g = new int[N][N];// 鄰接矩陣
    static int n, m;
    static int[] dis = new int[N];
    static int[] st = new int[N];
    
    public static int Prim(){
        Arrays.fill(dis, 0x3f3f3f3f);
        
        int res = 0;
        dis[1] = 0;
        for(int i = 0; i < n; i++){
            int t = -1;    
            int minv = INF;
            
            for(int j = 1; j <= n; j++){
                if(st[j] == 0 && minv > dis[j]){
                    t = j;
                    minv = dis[j];
                }
            }
            
            if(minv == INF) return INF;
            res += dis[t];
            st[t] = 1;
           
            for(int j = 1; j <= n; j++){
                dis[j] = Math.min(dis[j], g[t][j]);
            }
            
        }
        return res;
    }
    
    public static void main(String[] args) throws Exception{
        String[] s = in.readLine().split(" ");
        n = Integer.parseInt(s[0]);
        m = Integer.parseInt(s[1]);
        for(int i = 1; i <= n; i++){
            Arrays.fill(g[i], 0x3f3f3f3f);
        }
       
        for(int i = 0; i < m; i++){
            int a, b, w;
            String[] s1 = in.readLine().split(" ");
            a = Integer.parseInt(s1[0]);
            b = Integer.parseInt(s1[1]);
            w = Integer.parseInt(s1[2]);
            g[a][b] = g[b][a] = Math.min(g[a][b], w);
        }
        
        int res = Prim();
        if(res == INF) out.write("impossible\n");
        else out.write(res+"\n");
        out.flush();
    }    
}

另:Prim和Dijkstra一樣都可以使用優先隊列優化,但是寫着比較麻煩,而Kruskal實現簡單,效率又高,所以處理稀疏圖就用Kruskal了,本文不在介紹堆優化版Prim。


Kruskal

Kruskal適用於稀疏圖的原因是,該算法是將所有邊按升序排序,然後依次將未加入最小生成樹最小的邊加入最小生成樹

爲什麼這樣就可以得到一個圖的最小生成樹了呢?
反證法
當前最小邊連接了兩個連通塊, 一個是最小生成樹所在的連通塊,一個是未加入最小生成樹,但最終仍要加入其中的連通塊。
如圖:
在這裏插入圖片描述
看到上圖發綠光的邊了嗎:
如果不將(1,2)加入最小生成樹,最小生成樹所在的連通塊最終仍然要通過其他邊連接點(2,4,5)所連接的連通塊,而其他邊的權值一定大於(1,2),這樣總邊權就增大了,就不是最小生成樹了。所以不原諒她就會白白付出很多!花費的代價更多,就不是最小生成樹了,一定要原諒

代碼實現:

Kruskal算法實現起來就簡單多了,第一步排序第二步檢查當前最小邊所連接的兩個點是否都在最小生成樹的點集內,不在的話就將其加入最小生成樹。 這裏用並查集來判斷兩點是否在同一集合內

不知道並查集的請點擊:並查集

public static int find(int x){ // 很經典的並查集查找函數,此函數還實現了路徑壓縮
        if(p[x] != x) p[x] = find(p[x]);
        return p[x];
}

public static int kruskal1()throws IOException{
    Arrays.sort(g, 0, m-1);   // 對邊集排序,g數組存儲所有邊
        
    int cnt = 0, res = 0;// cnt表示加入最小生成樹的有幾條邊
                         // res表示最小生成樹的邊權
        
    for(int i = 1; i <= n; i++) p[i] = i;// 初始化並查集
        
    for(int i = 0; i < m; i++){
    	pair t = g[i]; 
     	int a = find(t.a); 
     	int b = find(t.b);
     	if(a != b){ // 如果a和b不在同一個集合,將a和b合併,將邊(a,b)加入最小生成樹
     		p[a] = b;
     		cnt ++;
     		res += t.w;
     	}
   }
        
        if(cnt != n - 1) return 0x3f3f3f3f;//如果少於n-1條則該圖不連通,無最小生成樹
        else return res;
    }

來個綠油油的習題檢測一下自己夠不夠綠吧:

Kruskal算法求最小生成樹

代碼:

import java.io.*;
import java.util.*;

public class Main{
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
    
    public static class pair implements Comparable<pair>{
        int a, b, w;
        pair(int u, int v, int x){
            a = u;
            b = v;
            w = x;
        }
        public int compareTo(pair p) { // 自定義類需要重寫比較器
    		return this.w - p.w;
    	}
    }

    static final int N = 200010, INF = 0x3f3f3f3f;
    static int n, m;
    static int[] p = new int[N];
    static pair[] g = new pair[N];
    
    public static int Int(String s){
        return Integer.parseInt(s);
    }
    
    public static int find(int x){
        if(p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    
    public static int kruskal1()throws IOException{
        Arrays.sort(g, 0, m-1);
        
        int cnt = 0, res = 0;
        
        for(int i = 1; i <= n; i++) p[i] = i;
        
        for(int i = 0; i < m; i++){
        	pair t = g[i];
        	int a = find(t.a);
        	int b = find(t.b);
        	if(a != b){
        		p[a] = b;
        		cnt ++;
        		res += t.w;
        	}
        }
        
        if(cnt != n - 1) return 0x3f3f3f3f;
        else return res;
    }
    
    public static void main(String[] args) throws IOException{
        String[] s = in.readLine().split(" ");
        n = Int(s[0]);
        m = Int(s[1]);
        for(int i = 0, j = 0; i < m; i++){
            String[] s1 = in.readLine().split(" ");
            g[j++] = new pair(Int(s1[0]), Int(s1[1]), Int(s1[2]));
        }
        
        int res = kruskal1();
        if(res == 0x3f3f3f3f) out.write("impossible\n");
        else out.write(res+"\n");
        out.flush();
    }
}

畫圖好累點個贊否?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章