K:圖相關的最小生成樹(MST)

相關介紹:

 根據樹的特性可知,連通圖的生成樹是圖的極小連通子圖,它包含圖中的所有頂點,但只有構成一棵樹的邊;生成樹又是圖的極大無迴路子圖,它的邊集是關聯圖中的全部頂點而又沒有造成迴路的邊。html

 一個有n個頂點的連通圖的生成樹只有n-1條邊。如有n個頂點而少於n-1條邊,則是非連通圖(將其想成有n個頂點的一條鏈,則其爲連通圖的條件是至少有n-1條邊);若多於n-1條邊,則必定造成迴路。值得注意的是,有n-1條邊的生成子圖,並不必定是生成樹。此處,介紹一個概念。:指的是邊帶有權值的圖。java

 在一個網的全部生成樹中,權值總和最小的生成樹,稱之爲最小代價生成樹,也稱爲最小生成樹。node

最小生成樹:

 根據生成樹的定義,具備n個頂點的連通圖的生成樹,有n個頂點和n-1條邊。所以,構造最小生成樹的準則有如下3條:算法

  1. 只能使用圖中的邊構造最小生成樹
  2. 當且僅當使用n-1條邊來鏈接圖中的n個頂點
  3. 不能使用產生迴路的邊

須要注意的一點是,儘管最小生成樹必定存在,但其並不必定是惟一的。如下介紹求圖的最小生成樹的兩個典型的算法,分別爲克魯斯卡爾算法(kruskal)和普里姆算法(prim)數據結構

克魯斯卡爾(Kruskal)算法:

 克魯斯卡爾算法是根據邊的權值遞增的方式,依次找出權值最小的邊創建的最小生成樹,而且規定每次新增的邊,不能形成生成樹有迴路,直到找到n-1條邊爲止。ide

基本思想:設圖G=(V,E)是一個具備n個頂點的連通無向網,T=(V,TE)是圖的最小生成樹,其中V是T的頂點集,TE是T的邊集,則構造最小生成樹的具體步驟以下:測試

  1. T的初始狀態爲T=(V,空集),即開始時,最小生成樹T是圖G的生成零圖ui

  2. 將圖G中的邊按照權值從小到大的順序依次選取,若選取的邊未使生成樹T造成迴路,則加入TE中,不然捨棄,直至TE中包含了n-1條邊爲止this

下圖演示克魯斯卡爾算法的構造最小生成樹的過程:spa

克魯斯卡爾算法構造最小生成樹的一個例子

其示意代碼以下:

相關代碼

package all_in_tree;

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

import algorithm.PathCompressWeightQuick_Union;
import algorithm.UF;

/**
 * 該類用於演示克魯斯卡爾算法的過程
 * @author 學徒
 *
 *因爲每次添加一條邊時,須要判斷所添加的邊是否會產生迴路,而回路的產生,當且僅當邊上的兩個節點處在同一個連通
 *分支上,爲此,可使用Union-Find算法來判斷邊上的兩個點是否處在同一個連通分支上
 *
 */
public class Kruskal
{
	//用於記錄節點的數目
	private int nodeCount;
	//用於判斷是否會造成迴路
	private UF unionFind;
	//用優先級隊列,每次最早出隊的是其權值最小的邊
	private Queue<Edge> q;
	//用於存儲圖的生成樹
	private Edge[] tree;
	/**
	 * 初始化一個圖的最小生成樹所需的數據結構
	 * @param n 圖的節點的數目
	 */
	public Kruskal(int n)
	{
		this.nodeCount=n;
		tree=new Edge[n-1];
		unionFind=new PathCompressWeightQuick_Union(n);
		Comparator<Edge> cmp=new Comparator<Edge>()
		{
			@Override
			public int compare(Edge obj1,Edge obj2)
			{
				int obj1W=obj1.weight;
				int obj2W=obj2.weight;
				if(obj1W<obj2W)
					return -1;
				else if(obj1W>obj2W)
					return 1;
				else
					return 0;
			}
		};
		q=new PriorityQueue<Edge>(11,cmp);
	}
	/**
	 * 用於添加一條邊
	 * @param edge 所要進行添加的邊
	 */
	public void addEdge(Edge edge)
	{
		q.add(edge);
	}
	
	/**
	 * 用於生成最小生成樹
	 * @return 最小生成樹的邊集合
	 */
	public Edge[] getTree()
	{
		//用於記錄加入圖的最小生成樹的邊的數目
		int edgeCount=0;
		//用於獲得最小生成樹
		while(!q.isEmpty()&&edgeCount<this.nodeCount-1)
		{
			//每次取出權值最小的一條邊
			Edge e=q.poll();
			//判斷是否產生迴路,當其不產生迴路時,將其加入到最小生成樹中
			int index1=unionFind.find(e.node1);
			int index2=unionFind.find(e.node2);
			if(index1!=index2)
			{
				tree[edgeCount++]=e;
				unionFind.union(e.node1, e.node2);
			}
		}
		return tree;
	}
}

/**
 * 測試用例所使用的類,該類的測試用例即爲上圖中中所示的Kruskal算法最小生成樹的構造
 * 過程的示例圖,且其節點編號從0開始,而不從1開始
 * @author 學徒
 *
 */
class Test
{
	public static void main(String[] args)
	{
		Kruskal k=new Kruskal(6);
		k.addEdge(new Edge(0,3,5));
		k.addEdge(new Edge(0,1,6));
		k.addEdge(new Edge(1,4,3));
		k.addEdge(new Edge(4,5,6));
		k.addEdge(new Edge(3,5,2));
		k.addEdge(new Edge(0,2,1));
		k.addEdge(new Edge(1,2,5));
		k.addEdge(new Edge(2,4,6));
		k.addEdge(new Edge(2,5,4));
		k.addEdge(new Edge(2,3,6));
		Edge[] tree=k.getTree();
		for(Edge e:tree)
		{
			System.out.println(e.node1+" --> "+e.node2+"  : "+e.weight);
		}
	}
}

/**
 * 圖的邊的數據結構
 * @author 學徒
 *
 */
class Edge
{
	//節點的編號
	int node1;
	int node2;
	//邊上的權值
	int weight;
	
	public Edge()
	{
	}
	public Edge(int node1,int node2,int weight)
	{
		this.node1=node1;
		this.node2=node2;
		this.weight=weight;
	}
}


運行結果:
0 --> 2  : 1
3 --> 5  : 2
1 --> 4  : 3
2 --> 5  : 4
1 --> 2  : 5

ps:上述代碼中所用到的Union-Find算法的相關代碼及解析,請點擊 K:Union-Find(並查集)算法 進行查看

分析 :該算法的時間複雜度爲O(elge),即克魯斯卡爾算法的執行時間主要取決於圖的邊數e,爲此,該算法適用於針對稀疏圖的操做

普里姆算法(Prim):

 爲描述的方便,在介紹普里姆算法前,給出以下有關距離的定義:

  1. 兩個頂點之間的距離:是指將頂點u鄰接到v的關聯邊的權值,即爲|u,v|。若兩個頂點之間無邊相連,則這兩個頂點之間的距離爲無窮大

  2. 頂點到頂點集合之間的距離:頂點u到頂點集合V之間的距離是指頂點u到頂點集合V中全部頂點之間的距離中的最小值,即爲|u,V|=$min|u,v| , v\in V$

  3. 兩個頂點集合之間的距離:頂點集合U到頂點集合V的距離是指頂點集合U到頂點集合V中全部頂點之間的距離中的最小值,記爲|U,V|=$min|u,V| , u\in U$

基本思想:假設G=(V,E)是一個具備n個頂點的連通網,T=(V,TE)是網G的最小生成樹。其中,V是R的頂點集,TE是T的邊集,則最小生成樹的構造過程以下:從U={u0},TE=$\varnothing$開始,必存在一條邊(u*,v*),u*$\in U$,v*$\in V-U$,使得|u*,v*|=|U,V-U|,將(u*,v*)加入集合TE中,同時將頂點v*加入頂點集U中,直到U=V爲止,此時,TE中必有n-1條邊(最小生成樹存在的狀況),最小生成樹T構造完畢。下圖演示了使用Prim算法構造最小生成樹的過程

prim算法構造最小生成樹的過程示意圖

其示意代碼以下:

相關代碼

package all_in_tree;
/**
 * 該類用於演示Prim算法構造最小生成樹的過程
 * @author 學徒
 *
 */
public class Prim
{
	//用於記錄圖中節點的數目
	private int nodeCount;
	//用於記錄圖的領接矩陣,其存儲對應邊之間的權值
	private int[][] graph;
	//用於記錄其對應節點是否已加入集合U中,若加入了集合U中,則其值爲true
	private boolean[] inU;
	//用於記錄其生成的最小生成樹的邊的狀況
	private Edge[] tree;
	//用於記錄其下標所對的節點的編號相對於集合U的最小權值邊的權值的狀況
	private int[] min;
	//用於記錄其下標所對的節點的最小權值邊所對應的集合U中的節點的狀況
	private int[] close;
	/**
	 * 用於初始化
	 * @param n 節點的數目
	 */
	public Prim(int n)
	{
		this.nodeCount=n;
		this.graph=new int[n][n];
		//初始化的時候,將各點的權值初始化爲最大值
		for(int i=0;i<n;i++)
		{
			for(int j=0;j<n;j++)
			{
				graph[i][j]=Integer.MAX_VALUE;
			}
		}
		this.inU=new boolean[n];
		this.tree=new Edge[n-1];
		this.min=new int[n];
		this.close=new int[n];
	}
	
	/**
	 *用於爲圖添加一條邊 
	 * @param edge 邊的封裝類
	 */
	public void addEdge(Edge edge)
	{
		int node1=edge.node1;
		int node2=edge.node2;
		int weight=edge.weight;
		graph[node1][node2]=weight;
		graph[node2][node1]=weight;
	}
	
	/**
	 * 用於獲取其圖對應的最小生成樹的結果
	 * @return 由最小生成樹組成的邊的集合
	 */
	public Edge[] getTree()
	{
		//用於將第一個節點加入到集合U中
		for(int i=1;i<nodeCount;i++)
		{
			min[i]=graph[0][i];
			close[i]=0;
		}
		inU[0]=true;
		//用於循環n-1次,每次循環添加一條邊進最小生成樹中
		for(int i=0;i<nodeCount-1;i++)
		{
			//用於記錄找到的相對於集合U中的節點的最小權值的節點編號
			int node=0;
			//用於記錄其相對於集合U的節點的最小的權值
			int mins=Integer.MAX_VALUE;
			//用於尋找其相對於集合U中最小權值的邊
			for(int j=1;j<nodeCount;j++)
			{
				if(min[j]<mins&&!inU[j])
				{
					mins=min[j];
					node=j;
				}
			}
			//用於記錄其邊的狀況
			tree[i]=new Edge(node,close[node],mins);
			//修改相關的狀態
			inU[node]=true;
			//修改其相對於集合U的狀況
			for(int j=1;j<nodeCount;j++)
			{
				if(!inU[j]&&graph[node][j]<min[j])
				{
					min[j]=graph[node][j];
					close[j]=node;
				}
			}
		}
		return tree;
	}
}

class Edge
{
	//節點的編號
	int node1;
	int node2;
	//邊上的權值
	int weight;
	
	public Edge()
	{
	}
	public Edge(int node1,int node2,int weight)
	{
		this.node1=node1;
		this.node2=node2;
		this.weight=weight;
	}
}

/**
 * 測試用例所使用的類,該類的測試用例即爲上圖中中所示的Prim算法最小生成樹的構造
 * 過程的示例圖,且其節點編號從0開始,而不從1開始
 * @author 學徒
 *
 */
class Test
{
	public static void main(String[] args)
	{
		Prim k=new Prim(6);
		k.addEdge(new Edge(0,3,5));
		k.addEdge(new Edge(0,1,6));
		k.addEdge(new Edge(1,4,3));
		k.addEdge(new Edge(4,5,6));
		k.addEdge(new Edge(3,5,2));
		k.addEdge(new Edge(0,2,1));
		k.addEdge(new Edge(1,2,5));
		k.addEdge(new Edge(2,4,6));
		k.addEdge(new Edge(2,5,4));
		k.addEdge(new Edge(2,3,5));
		Edge[] tree=k.getTree();
		for(Edge e:tree)
		{
			System.out.println(e.node1+" --> "+e.node2+"  : "+e.weight);
		}
	}
}


運行結果以下:
2 --> 0  : 1
5 --> 2  : 4
3 --> 5  : 2
1 --> 2  : 5
4 --> 1  : 3

總結:kruskal算法的時間複雜度與求解最小生成樹的圖中的邊數有關,而prim算法的時間複雜度與求解最小生成樹的圖中的節點的數目有關。爲此,Kruskal算法更加適用於稀疏圖,而prim算法適用於稠密圖。當e>=n^2時,kruskal算法比prim算法差,但當e=O(n^2)時,kruskal算法卻比prim算法好得多。

回到目錄|·(工)·)

相關文章
相關標籤/搜索