最小生成樹(Prim算法和Kruskal算法算法詳解)

前言

在數據結構與算法的圖論中,(生成)最小生成樹算法是一種經常使用而且和生活貼切比較近的一種算法。可是可能不少人對概念不是很清楚。咱們看下百度百科對於最小生成樹定義java

一個有 n 個結點的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的全部 n 個結點,而且有保持圖連通的最少的邊。 最小生成樹能夠用kruskal(克魯斯卡爾)算法或prim(普里姆)算法求出。算法

通俗易懂的講就是最小生成樹包含原圖的全部節點而只用最少的邊最小的權值距離。由於n個節點最少須要n-1個邊聯通,而距離就須要採起某種策略選擇恰當的邊。數組

從定義上分析,最小生成樹實際上是一種能夠看做是樹的結構。而最小生成樹的結構來源於圖(尤爲是有環狀況)。經過這個圖咱們使用某種算法造成最小生成樹的算法就能夠叫作最小生成樹算法。具體實現上有兩種實現方法、策略分別爲kruskal算法和prim算法。數據結構

學習最小生成樹實現算法以前咱們要先高清最小生成樹的結構和意義所在。咱麼首先根據一些圖更好的祝你理解。ide

一個故事

在中國城市道路規劃中,是一門很須要科學的研究(只是假設學習沒必要當真)。城市道路鋪設可能經歷如下幾個階段。函數

  • 初始,各個城市沒有高速公路(鐵路)。城市沒有!
  • 政府打算各個城市鋪設公路(鐵路),每一個城市都想成爲交通樞紐,快速到達其餘城市!可是這種狀況下國家集體資源跟不上、造價太昂貴。而且形成巨大浪費!
  • 最終國家選擇一些主要城市進行聯通,有個別城市只能稍微繞道而行,而繞道太遠的、人流量多的國家考慮新建公路(鐵路)。適當提升效率。

在這裏插入圖片描述
而隨着國家科技互聯網的進步,須要鋪設 高科技黃金外嵌光纜管道 (黃金誇張)聯通整個國家使得信息可以快速傳統、聯通。(注意,我們的通道是黃金的)對於有些可能重複的環。勢必形成浪費。

因此咱們要從有環圖中選取代價和最小的路線一方面代價最小(總距離最小最省黃金)另外一方面聯通全部城市學習

然而根據上圖咱們能夠獲得如下最小生成樹: this

在這裏插入圖片描述
惟物辯證法認爲

  • 問題的主要矛盾對問題起着決定性做用。主要矛盾次要矛盾相互影響,相互滲透,必定程度能夠相互轉化。故咱們看問題要抓關鍵、找核心
  • 公路時代城市聯通的主要矛盾是時間慢,而造價相比運輸時間是次要矛盾。因此在公路時代咱們儘可能使得城市可以直接聯通,縮短城市聯繫時間。而稍微考慮建路成本!隨着科技發展、信息傳輸相比公路運輸很快,從而事件的主要矛盾從運輸時間轉變爲造價成本。因此咱們會關注聯通全部點的路程(最短)。這就用到最小生成樹算法。

而相似的還有局部區域島嶼聯通修橋,海底通道這些高成本的都多多少少會運用。spa

Kruskal算法

上面介紹了最小生成樹是什麼,可是咱們須要掌握和理解最小生成樹如何造成。給你一個圖,生成一個最小生成樹,固然須要必定規則。而在實現最小生成樹方面有prim和kruskal算法,這兩種算法的策略有所區別,可是時間複雜度一致。code

百度百科定義的基本思想

先構造一個只含 n 個頂點、而邊集爲空的子圖,把子圖中各個頂點當作各棵樹上的根結點,以後,從網的邊集 E 中選取一條權值最小的邊,若該條邊的兩個頂點分屬不一樣的樹,則將其加入子圖,即把兩棵樹合成一棵樹,反之,若該條邊的兩個頂點已落在同一棵樹上,則不可取,而應該取下一條權值最小的邊再試之。依次類推,直到森林中只有一棵樹,也即子圖中含有 n-1 條邊爲止。

簡而言之,Kruskal算法進行調度的單位是邊,它的信仰爲:全部邊能小則小,算法的實現方面和並查集(不相交集合)很像,要用到並查集判斷兩點是否在同一集合。

而算法的具體步驟爲:

  1. 將邊(以及2頂點)的對象依次加入集合(優先隊列)q1中。初始全部點相互獨立
  2. 取出當前q1最小邊,判斷邊的兩點是否聯通。
  3. 若是聯通,跳過,若是不連通,則使用union(並查集合並)將兩個頂點合併。這條邊被使用(能夠儲存或者計算數值)。
  4. 重複2,3操做直到集合(優先隊列)q1爲空。此時被選擇的邊構成最小生成樹。

在這裏插入圖片描述
在這裏插入圖片描述

Prim算法

除了Kruskal算法之外,普里姆算法(Prim算法)也是經常使用的最小生成樹算法。雖然在效率上差很少。可是貪心的方式和Kruskal徹底不一樣。prim算法的核心信仰是:從已知擴散尋找最小。它的實現方式和Dijkstra算法類似但稍微有所區別,Dijkstra是求單源最短路徑。而每計算一個點須要對這個點重新更新距離。而prim甚至不用更新距離。直接找已知點的鄰邊最小加入便可!

對於具體算法具體步驟,大體爲:

  1. 尋找圖中任意點,以它爲起點,它的全部邊V加入集合(優先隊列)q1,設置一個boolean數組bool[]標記該位置已經肯定。
  2. 從集合q1找到距離最小的那個邊v1判斷邊另外一點p是否被標記(訪問),若是p被標記說明已經肯定那麼跳過,若是未被標(訪問)記那麼標記該點p,而且與p相連的未知點(未被標記)構成的邊加入集合q1邊v1(能夠進行計算距離之類,該邊構成最小生成樹) .
  3. 重複1,2直到q1爲空,構成最小生成樹 !

大致步驟圖解爲:

在這裏插入圖片描述
在這裏插入圖片描述

由於prim從開始到結束一直是一個總體在擴散,因此不須要考慮兩棵樹合併的問題,在這一點實現上稍微方便了一點。

固然,要注意的是最小生成樹並不惟一,甚至同一種算法生成的最小生成樹均可能有所不一樣,可是相同的是不管生成怎樣的最小生成樹:

  • 可以保證全部節點連通(可以知足要求和條件)
  • 可以保證全部路徑之和最小(結果和目的相同)
  • 最小生成樹不惟一,可能多樣的
    在這裏插入圖片描述

代碼實現

上面分析了邏輯實現。下面咱們用代碼簡單實現上述的算法。

prim
package 圖論;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;

public class prim {

	public static void main(String[] args) {
		int minlength=0;//最小生成樹的最短路徑長度
		int max=66666;
		String cityname[]= {"北京","武漢","南京","上海","杭州","廣州","深圳"};
		int city[][]= {
				{ max, 8, 7, max, max, max, max }, //北京和武漢南京聯通
				{ 8, max,6, max,9, 8,max }, //武漢——北京、南京、杭州、廣州
				{ 7, 6, max, 3,4, max,max }, //南京——北京、武漢、上海、杭州
				{ max, max,3, max,2, max,max }, //上海——南京、杭州
				{ max, 9,4, 2,max, max,10 }, //杭州——武漢、南京、上海、深圳
				{ max, 8,max, max,max, max,2 }, //廣州——武漢、深圳
				{ max, max,max, max,10,2,max }//深圳——杭州、廣州
		};// 地圖

		boolean istrue[]=new boolean[7];
		//南京
		Queue<side>q1=new PriorityQueue<side>(new Comparator<side>() {
			public int compare(side o1, side o2) {
				// TODO Auto-generated method stub
				return o1.lenth-o2.lenth;
			}
		});
		for(int i=0;i<7;i++)
		{
			if(city[2][i]!=max)
			{
				istrue[2]=true;
				q1.add(new side(city[2][i], 2, i));
			}
		}		
		while(!q1.isEmpty())
		{
			side newside=q1.poll();//拋出
			if(istrue[newside.point1]&&istrue[newside.point2])
			{
				continue;
			}
			else {
				if(!istrue[newside.point1])
				{
					istrue[newside.point1]=true;
					minlength+=city[newside.point1][newside.point2];
					System.out.println(cityname[newside.point1]+" "+cityname[newside.point2]+" 聯通");
					for(int i=0;i<7;i++)
					{
						if(!istrue[i])
						{
							q1.add(new side(city[newside.point1][i],newside.point1,i));
						}
					}
				}
				else {
					istrue[newside.point2]=true;
					minlength+=city[newside.point1][newside.point2];
					System.out.println(cityname[newside.point2]+" "+cityname[newside.point1]+" 聯通");
					for(int i=0;i<7;i++)
					{
						if(!istrue[i])
						{
							q1.add(new side(city[newside.point2][i],newside.point2,i));
						}
					}
				}
			}
			
		}
		System.out.println(minlength);		
	}
	
	static class side//邊 {
		int lenth;
		int point1;
		int point2;
		public side(int lenth,int p1,int p2) {
			this.lenth=lenth;
			this.point1=p1;
			this.point2=p2;
		}
	}

}

複製代碼

實現效果:

在這裏插入圖片描述

Kruskal:
package 圖論;

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

import 圖論.prim.side;
/* * 做者:bigsai(公衆號) */
public class kruskal {

	static int tree[]=new int[10];//bing查集
	public static void init() {
		for(int i=0;i<10;i++)//初始
		{
			tree[i]=-1;
		}
	}
	public static int search(int a)//返回頭節點的數值 {
		if(tree[a]>0)//說明是子節點
		{
			return tree[a]=search(tree[a]);//路徑壓縮
		}
		else
			return a;
	}
	public static void union(int a,int b)//表示 a,b所在的樹合併小樹合併大樹(不重要) {
		int a1=search(a);//a根
		int b1=search(b);//b根
		if(a1==b1) {//System.out.println(a+"和"+b+"已經在一棵樹上");
		}
		else {
		if(tree[a1]<tree[b1])//這個是負數,爲了簡單減小計算,不在調用value函數
		{
			tree[a1]+=tree[b1];//個數相加 注意是負數相加
			tree[b1]=a1;       //b樹成爲a的子樹,直接指向a;
		}
		else
		{
			tree[b1]+=tree[a1];//個數相加 注意是負數相加
			tree[a1]=b1;       //b樹成爲a的子樹,直接指向a;
		}
		}
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		init();
		int minlength=0;//最小生成樹的最短路徑長度
		int max=66666;
		String cityname[]= {"北京","武漢","南京","上海","杭州","廣州","深圳"};
		boolean jud[][]=new boolean[7][7];//加入邊須要防止重複 好比 ba和ab等價的
		int city[][]= {
				{ max, 8, 7, max, max, max, max }, 
				{ 8, max,6, max,9, 8,max }, 
				{ 7, 6, max, 3,4, max,max }, 
				{ max, max,3, max,2, max,max }, 
				{ max, 9,4, 2,max, max,10 }, 
				{ max, 8,max, max,max, max,2 }, 
				{ max, max,max, max,10,2,max }
		};// 地圖
		boolean istrue[]=new boolean[7];
		//南京
		Queue<side>q1=new PriorityQueue<side>(new Comparator<side>() {//優先隊列存邊+
			public int compare(side o1, side o2) {
				// TODO Auto-generated method stub
				return o1.lenth-o2.lenth;
			}
		});
		for(int i=0;i<7;i++)
		{
			for(int j=0;j<7;j++)
			{
				if(!jud[i][j]&&city[i][j]!=max)//是否加入隊列
				{
					jud[i][j]=true;jud[j][i]=true;
					q1.add(new side(city[i][j], i, j));
				}
			}
		}
		while(!q1.isEmpty())//執行算法
		{
			side newside=q1.poll();
			int p1=newside.point1;
			int p2=newside.point2;
			if(search(p1)!=search(p2))
			{
				union(p1, p2);
				System.out.println(cityname[p1]+" "+cityname[p2]+" 聯通");
				minlength+=newside.lenth;
			}
		}
		System.out.println(minlength);
		

	}
	static class side//邊 {
		int lenth;
		int point1;
		int point2;
		public side(int lenth,int p1,int p2) {
			this.lenth=lenth;
			this.point1=p1;
			this.point2=p2;
		}
	}
}

複製代碼
kruskal

在這裏插入圖片描述

總結

最小生成樹算法理解起來也相對簡單,實現起來也不是很難。Kruskal和Prim主要是貪心算法的兩種角度。一個從總體開始找最小邊,遇到關聯不斷合併,另外一個從局部開始擴散找身邊的最小不斷擴散直到生成最小生成樹。在學習最小生成樹以前最好學習一下dijkstra算法和並查集,這樣在實現起來可以快一點,清晰一點。

最後,若是你那天真的得到一大筆資金去修建這麼一條昂貴的黃金路線,能夠適當採起此方法,另外剩下的大批,,苟富貴,勿相忘。。

若是感受還行,還請點個贊,關注一下吧,關注筆者公衆號: bigsai回覆數據結構便可得到數據結構的學習資料和視頻一份!

相關文章
相關標籤/搜索