1. 程式人生 > >並查集在kruskal演算法中應用

並查集在kruskal演算法中應用

    在無向圖論問題中,經常需要得到圖的最小生成樹,用於解決這個問題有兩個經典演算法:kruskal和prim,前者用於稀疏圖,後者用於稠密圖。kruskal演算法的核心思想是貪心,按照權值順序,先選取權值最小的邊,再選取權值次小的邊,依此類推,直到所選邊足夠把所有的點連線起來,這時邊數為節點數-1。但有個選邊的前提,那就是待選邊不能和已選邊組成迴路。至此,kruskal演算法要解決的問題便成了圖的連通分支判斷問題。而並查集正是用於求解該問題的有效方法。

    並查集是一種樹形資料結構,其實現是定義一個長度為N+1(N為圖的節點個數)的陣列,陣列元素值初始化為下標,表示所有的節點都初始化為由一個單點組成的樹,每個節點都是自身的祖先。那麼並查集如何判斷兩個連通分支是否是一個連通分支(即構成迴路)呢?方法就是查詢兩棵樹的祖先,如果祖先相同,則表示這兩個連通分支是一個連通分支。除了查詢,並查集還有一個方法用來合併兩個連通分支,實現就是把A樹(連通分支)的祖先設定為B樹(連通分支)的祖先,反過來也行。

    kruskal演算法通過不斷地選邊,然後查詢這條邊的兩個節點所在的連通分支,再合併分支,最終得到了一顆最小生成樹。演算法的時間複雜度為O(NlogN)。

   JAVA實現如下:

public class Kruskal {
	
	/**
	 * 查詢分支中某個元素的祖先,當祖先為自身時停止查詢
	 * @param x
	 * @param branch
	 * @return
	 */
	public static int find(int x,int[] branch){
		while(x!=branch[x]){
			x = branch[x];
		}
		return x;
	}
	
	/**
	 * 合併分支,設branch[a]的祖先為b
	 * @param x
	 * @param branch
	 */
	public static void join(int a,int b,int[] branch){
		branch[a]=b;
	}
	
	public static void main(String args[]){
		Scanner in=new Scanner(System.in);
		//n表示節點數,m表示邊數
		int n = in.nextInt();
		int m = in.nextInt();
		Edge[] edges = new Edge[m];
		Edge edge = null;
		//讀入每條邊的資訊
		for(int i=0;i<m;i++){
			edge = new Edge();
			edge.a = in.nextInt();
			edge.b = in.nextInt();
			edge.w = in.nextInt();
			edges[i]=edge;
		}
		in.close();
		Arrays.sort(edges);
		
		/**
		 * 並查集初始化為N個分支,每個人都是自己的祖先
		 */
		int[] branch = new int[n+1];
		for(int i=0;i<=n;i++){
			branch[i]=i;
		}
		
		int selected = 0;
		int weightTotal = 0;
		
		//無需回溯
		for(int i=0;i<m && selected<n-1;i++){
			//查詢兩個結點的祖先
			int rootA = find(edges[i].a,branch);
			int rootB = find(edges[i].b,branch);
			if(rootA!=rootB){
				//合併兩個分支,可以把A分支的祖先設為B分支的祖先,反之亦可
				join(rootA,rootB,branch);
				weightTotal += edges[i].w;
				selected ++;
			}
		}
		
		
		System.out.println(weightTotal);
	}
}

class Edge implements Comparable<Edge>{
	public int a;
	public int b;
	public int w;
	@Override
	public int compareTo(Edge e) {
		return this.w - e.w;
	}
}