算法筆記15:最小生成樹(Prim算法和Kruskal算法)

最小生成樹和其算法原理

加權圖是指一種每條邊都有權重的圖。圖的生成樹是它的一棵含有其所有頂點無環連通子圖。最小生成樹是指所有邊的權重最小的樹。如下圖所示,該圖的最小生成樹在圖中藍色標出,本文的目的就是設計算法得到加權圖的最小生成樹。
在這裏插入圖片描述
切分定理:在一副加權圖中,給定一種切分,它的橫切邊中的權重最小邊必定屬於圖的最小生成樹。橫切邊是指將圖分爲沒有交集的兩部分,連接這兩部分的邊。

所有最小生成樹算法的本質都是重複的利用切分定理的貪心算法:初始狀態下所有邊均爲灰色,找到一種切分,其產生的橫切邊均不爲黑色,將權重最小的橫切邊標記爲黑色,反覆執行直到標記了V-1條黑色的邊。

爲了實現這種算法,先實現帶權重的邊和加權圖。

/**
 * 帶權重的邊
 * @author XY
 *
 */
public class WeightedEdage implements Comparable<WeightedEdage>{
	private int v;
	private int w;
	private double weight;
	public WeightedEdage(int v,int w,double weight){
		this.v=v;
		this.w=w;
		this.weight=weight;
	}
	public int either(){
		return v;
	}
	public int other(int vertex){
		if(v==vertex) return w;
		if(w==vertex) return v;
		else throw new IllegalArgumentException("no this vertex");
	}
	public double weight(){
		return this.weight;
	}
	@Override
	public int compareTo(WeightedEdage o) {//爲了權重比較方便直接實現Comparable接口
		if(this.weight>o.weight) return 1;
		if(this.weight<o.weight) return -1;
		return 0;
	}
	@Override
	public String toString() {
		return "vertex: "+v+" "+w+",weight: "+weight;
	}
	public static void main(String[] args) {//測試用例
		WeightedEdage edage=new WeightedEdage(0, 2, 0.52);
		WeightedEdage edage1=new WeightedEdage(3, 2, 0.2);
		int v=edage.either();
		int w=edage.other(v);
		System.out.println(v);
		System.out.println(w);
		System.out.println(edage);
		System.out.println(edage.compareTo(edage));
	}

}

下面是加權圖的實現

import java.io.File;
import java.util.ArrayList;
import java.util.Scanner;
/**
 * 加權圖
 * 和無向圖的實現完全一致,唯一不同是鄰接表中存儲的是連接該頂點的邊而不是頂點
 * @author XY
 *
 */
public class EdageWeightedGraph {
	private int V;
	private int E;
	private ArrayList<WeightedEdage>[] adj;
	public EdageWeightedGraph(int v){
		this.V=v;
		adj=(ArrayList<WeightedEdage>[])new ArrayList[v];
		for (int i = 0; i < adj.length; i++) {
			adj[i]=new ArrayList<WeightedEdage>();
		}
	}
	public EdageWeightedGraph(Scanner scan){
		this(scan.nextInt());
		int e=scan.nextInt();
		for (int i = 0; i < e; i++) {
			int v=scan.nextInt();
			int w=scan.nextInt();
			double weight=scan.nextDouble();
			addEdage(v, w, weight);
		}
	}
	public void addEdage(int v,int w,double weight){
		WeightedEdage edage=new WeightedEdage(v, w, weight);
		adj[v].add(edage);
		adj[w].add(edage);
		E++;
	}
	
	public Iterable<WeightedEdage> adj(int v){//返回相鄰邊
		return adj[v];
	}
	public Iterable<WeightedEdage> edages(){//返回所有邊
		ArrayList<WeightedEdage> list=new ArrayList<WeightedEdage>();
		for (int i = 0; i < adj.length; i++) {
			for(WeightedEdage edage:adj[i]){
				if(!list.contains(edage))
					list.add(edage);
			}
		}
		return list;
	}
	public int V(){
		return this.V;
	}
	public int E(){
		return this.E;
	}
	@Override
	public String toString() {
		StringBuffer sb=new StringBuffer();
		sb.append("V: "+V+" E: "+E+"\n");
		for(WeightedEdage edage:edages()){
			sb.append(edage+"\n");
		}
		return sb.toString();
	}
	public static void main(String[] args) throws Exception {//測試用例
		EdageWeightedGraph wgraph=new EdageWeightedGraph(new Scanner
				(new File("E:"+File.separator+"wedagegraph.txt")));
		for(WeightedEdage edage:wgraph.adj(0))
			System.out.println(edage);
		System.out.println(wgraph.V());
	}
	

}

Prim算法

根據前面講到的切分定理和最小生成樹算法的本質,我們需要一個boolean[]來標記頂點是否進入最小生成樹,如果一條邊的兩個頂點都被標記意味着該邊不屬於進入最小生成樹的邊的候選:橫切邊。使用一個集合保存進入最小生成樹的邊。使用優先序列動態加入邊和得到權重最小邊。
Prim算法是從任意一個頂點開始,此頂點加入最小生成樹,其所有邊加入優先序列,刪除得到權重最小邊,判斷權重最小邊是否是橫切邊,如果是該邊和另一個頂點加入最小生成樹,如果最小邊不是橫切邊則判斷次小邊直到找到屬於橫切邊的邊並加入最小生成樹。將新加入的頂點的相鄰邊全部加入優先序列,取出最小邊重複前述步驟。

延遲Prim算法

延遲Prim算法的內容就是前述的Prim算法。

import java.io.File;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Scanner;
/**
 * 演示Prim算法:最小生成樹
 * 從0開始每次尋找橫切邊中的最小權重邊加入最小生成樹,兩個頂點都在最小生成樹中的邊不是橫切邊
 * @author XY
 *
 */
public class LazyPrim {
	private PriorityQueue<WeightedEdage> pq;//保存邊,得到最小權重邊
	private LinkedList<WeightedEdage> queue=null;//最小生成樹結果
	private boolean[] marked;//標記是否進入最小生成樹
	private double weight;
	public LazyPrim(EdageWeightedGraph wgraph){
		pq=new PriorityQueue<WeightedEdage>();
		queue=new LinkedList<WeightedEdage>();
		marked=new boolean[wgraph.V()];
		for(WeightedEdage edage:wgraph.adj(0))
			pq.add(edage);
		marked[0]=true;
		while(!pq.isEmpty()){
			WeightedEdage edage=pq.poll();
			int v=edage.either();
			int w=edage.other(v);
			if(marked[v] && marked[w])
				continue;//非橫切邊,失效
			queue.add(edage);
			weight+=edage.weight();
			if(marked[v]) {
				marked[w]=true;
				visit(wgraph, w);
			}else {
				marked[v]=true;
				visit(wgraph, v);
			}			
		}
	}
	
	private void visit(EdageWeightedGraph wgraph,int v){
		for(WeightedEdage edage:wgraph.adj(v))
			if(!pq.contains(edage))
				pq.add(edage);
	}
	public double weight(){
		return this.weight;
	}
	public Iterable<WeightedEdage> edages(){//最小生成樹結果
		return queue;
	}
	public static void main(String[] args) throws Exception {
		EdageWeightedGraph wgraph=new EdageWeightedGraph(new Scanner
				(new File("E:"+File.separator+"wedagegraph.txt")));
		LazyPrim prim=new LazyPrim(wgraph);
		for (WeightedEdage edage:prim.edages()) {
			System.out.println(edage);
		}
		System.out.println(prim.weight());		
	}
}

即時Prim算法

即使Prim算法和延時Prim算法的唯一區別就是延遲Prim算法的優先序列中保存了每個頂點的所有橫切邊(非橫切邊會失效),那麼優先序列的大小就是E。但實際上只有每個頂點的最小橫切邊纔會有機會進入最小生成樹,其他變遲早會失效,這樣的話就可以減少優先序列的大小:每個頂點的最小橫切邊進入優先序列,爲此使用索引優先序列,索引位頂點,內容爲對應頂點的最小橫切邊。索引優先序列的實現參考索引優先序列中的最小索引優先序列。


import java.io.File;
import java.util.LinkedList;
import java.util.Scanner;
import linmin.Indexpriority;
/**
 * 即使Prim算法:最小生成樹
 * 和演示Prim算法的區別就是優先序列中只存在V個元素,對應的是連接每個頂點的邊的最小權重,
 * 而延時Prim中優先序列保存了所有的邊,E個元素。因爲對於每個頂點而言只會選擇權重最小的
 * 橫切邊進入最小生成樹,那麼權重更大的邊會失效,但依然在優先序列中參與優先序列的堆有序
 * 過程。這裏使用了edages[]和weights[]保存每個頂點的權重最小邊,他們的作用是得到最後的
 * 最小生成樹,改變索引優先序列Indexpriority的API,實現get方法去實現visit()方法中
 * 的更新的if語句,再使用隊列保存進入最小生成樹的邊,也可以實現本算法
 * @author XY
 *
 */
public class Prim {
	private Indexpriority<Double> pq;//索引優先序列,保存edages[]中的邊的權重。
	private boolean[] marked;//是否進入最小生成樹
	private WeightedEdage[] edages;//最小生成樹到索引點的邊中權重最小的邊。
	private double[] weights;//edages[]對應索引邊的權重
	public Prim(EdageWeightedGraph wgraph){
		pq=new Indexpriority<Double>(wgraph.V());
		marked=new boolean[wgraph.V()];
		edages=new WeightedEdage[wgraph.V()];
		weights=new double[wgraph.V()];
		for (int i = 0; i < edages.length; i++) {
			edages[i]=null;//初始爲0
			weights[i]=Double.POSITIVE_INFINITY;//初始爲無窮大
		}
		pq.insert(0, 0.0);//從0開始,第一個進入最小生成樹,邊的權重爲0;
		weights[0]=0.0;
		while(!pq.isEmpty()){
			visit(wgraph, pq.delMin());
		}
	}
	public void visit(EdageWeightedGraph wgraph,int v){
		marked[v]=true;
		for(WeightedEdage edage:wgraph.adj(v)){
			int w=edage.other(v);
			if(marked[w]) continue;
			if(edage.weight()<weights[w]){//如果連接該點的邊存在更小權重的邊則更新
				weights[w]=edage.weight();
				edages[w]=edage;
				pq.insert(w, weights[w]);
			}
		}
	}
	public Iterable<WeightedEdage> edages(){
		LinkedList<WeightedEdage> queue=new LinkedList<WeightedEdage>();
		for(WeightedEdage e:edages)
			if(e!=null) 
				queue.add(e);
				
		return queue;
	}
	public double weight(){
		double w=0;
		for (double i:weights) {
			w+=i;
		}
		return w;
	}
	public static void main(String[] args) throws Exception {
		EdageWeightedGraph wgraph=new EdageWeightedGraph(new Scanner
				(new File("E:"+File.separator+"wedagegraph.txt")));
		Prim prim=new Prim(wgraph);
		System.out.println(prim.weight());		
	}
}

Kruskal算法

Prim算法是從一個頂點開始連續的往下走完成最小生成樹,和Prim算法不一樣,Kruskal算法是分開的,首先形成的是最小生成樹的各個部分,最後各個部分連接完成最小生成樹。Kruskal算法的原理是將所有的邊依次刪除取最小邊,判斷最小邊是否屬於橫切邊,如果是那麼最小邊肯定會進入最後的最小生成樹,由於最小生成樹的生長過程不連續,需要使用union-find算法來判斷是否是橫切邊(union-find爲最小生成樹實現,選取的邊的兩個定點在union-find中連接,如果邊的兩個定點已經連接說明該邊不是橫切邊(沒有連接最小生成樹和非最小生成樹))並將頂點加入最小生成樹。union-find算法見union-find算法

import java.io.File;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Scanner;
import linmin.QUuf;
/**
 * 最小生成樹算法:Kruskal算法
 * 所有邊進入優先隊列,每次都選擇最小邊進入最小生成樹,爲了確定最小邊
 * 是橫切邊,所以使用union-find
 * @author XY
 *
 */
public class Kruskal {
	private PriorityQueue<WeightedEdage> pq;//優先隊列取出權重最小邊
	private LinkedList<WeightedEdage> queue;//保存最小生成樹的邊
	private QUuf uf;//最小生成樹的union-find,判斷邊是否會在最小生成樹中形成環
	private double weight=0.0;//最小生成樹的權重
	public Kruskal(EdageWeightedGraph wgraph){
		pq=new PriorityQueue<WeightedEdage>();
		queue=new LinkedList<WeightedEdage>();
		uf=new QUuf(wgraph.V());
		for (int i = 0; i < wgraph.V(); i++) {
			for(WeightedEdage e:wgraph.adj(i))
				if(!pq.contains(e))
					pq.add(e);			
		}
		while(!pq.isEmpty()){
			WeightedEdage e=pq.poll();//取出權重最小邊
			int v=e.either();
			int w=e.other(v);
			if(uf.isconnect(v, w)) continue;
			//判斷最小生成樹中兩點是否聯通,若連通則此邊形成環,失效
			uf.union(v, w);
			queue.add(e);
			weight+=e.weight();
		}		
	}
	public Iterable<WeightedEdage> edages(){
		return queue;
	}
	public double weight(){
		return weight;
	}
	public static void main(String[] args) throws Exception {
		EdageWeightedGraph wgraph=new EdageWeightedGraph(new Scanner
				(new File("E:"+File.separator+"wedagegraph.txt")));
		Kruskal k=new Kruskal(wgraph);
		for (WeightedEdage e:k.edages()) {
			System.out.println(e);
		}
		System.out.println(k.weight());
	}

}

最大生成樹問題

最小生成樹算法解決最大生成樹問題的3種方法:
1 將所有的權重取相對值,得到的最小生成樹就是最大生成樹,得到的最小生成樹的權重的相反數是最大生成樹的權重
2 改變WeightedEdage的compareTo()方法,改變兩個返回值
3 改變最小生成樹算法中的if()語句的不等式的方向。
後兩種方法的原理是一致的。

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