圖論算法之最小生成樹
最小生成樹
定義
一幅加權圖的最小生成樹(MST)是它的一棵權值(樹中所有邊的權值之和)最小的生成樹。
原理
圖的一種切分是將圖中所有頂點分爲兩個非空且不重疊的兩個集合。橫切邊是一條連接兩個屬於不同集合的頂點的邊。(下圖中的紅邊即爲橫切邊)
(切分定理):在一幅加權圖中,給定任意的切分,它的橫切邊中的權重最小者必然屬於圖中的最小生成樹。
由此可知我們只需要使用切分定理找到最小生成樹的一條邊,不斷重複直到找到最小生成樹的所有邊。
數據類型基本實現
帶權重的邊的數據類型
package chapter4;
/**
* 帶權重的邊的數據類型
*/
public class Edge implements Comparable<Edge> {
private final int v;//頂點之一
private final int w;//另外一個頂點
private final double weight;//邊的權重
Edge(int v, int w, double weight){
this.v=v;
this.w=w;
this.weight=weight;
}
public double weight(){
return weight;
}
/** 查詢邊的一個頂點 **/
public int either(){
return v;
}
/** 查詢邊的另外一個頂點 **/
public int other(int vertex) {
if (vertex == v) {
return w;
} else if (vertex == w) {
return v;
} else {
throw new RuntimeException("Inconsistent edge");
}
}
@Override
public int compareTo(Edge that) {
if(this.weight()<that.weight()){
return -1;
}else if(this.weight()>that.weight()){
return 1;
}else {
return 0;
}
}
public String toString(){
return String.format("%d-%d %.2f",v,w,weight);
}
}
加權無向圖的數據類型
package chapter4;
import java.util.ArrayList;
import java.util.LinkedList;
public class EdgeWeightedGraph {
private final int V;//頂點總數
private int E;//邊的總數
private LinkedList<Edge>[] adj;//鄰接表
public EdgeWeightedGraph(int V){
this.V=V;
this.E=0;
adj=new LinkedList[V];
for (int v = 0; v < V; v++) {
adj[v]=new LinkedList<>();
}
}
public int V(){
return V;
}
public int E(){
return E;
}
/** 鄰接表的構建 **/
public void addEdge(Edge e){
int v=e.either();
int w=e.other(v);
adj[v].addFirst(e);
adj[w].addFirst(e);
E++;
}
/** 查詢與某個頂點v相連接的邊 **/
public Iterable<Edge> adj(int v){
return adj[v];
}
/** 查詢鄰接表所有的邊 **/
public Iterable<Edge> edges(){
ArrayList<Edge> list = new ArrayList<>();
for (int v = 0; v < V; v++) {
for (Edge edge : adj[v]) {
if(!list.contains(edge)){
list.add(edge);
}
}
}
return list;
}
}
Prim算法延時實現
算法思想:每次將下一條連接樹中的頂點與不在樹中的頂點且權重最小的邊加入最小生成樹中。
原理
每當向樹中添加了一條邊之後,也向樹中添加了一個頂點,就要將連接這個頂點和其他所有不在樹中的頂點的邊加入優先隊列(新加入樹中的頂點與其他已經在樹中頂點的所有邊都失效),然後再取出優先隊列中的非失效邊,將其添加到最小生成樹中。
最小生成樹的Prim算法實現
package chapter4;
import edu.princeton.cs.algs4.MinPQ;
import edu.princeton.cs.algs4.Queue;
public class LazyPrimMST {
private boolean[] marked;//最小生成樹的頂點,true即爲該頂點已被添加到最小生成樹中
private Queue<Edge> mst;//最小生成樹的邊
private MinPQ<Edge> pq;//橫切邊(包括是小的邊),最小權值優先隊列
public LazyPrimMST(EdgeWeightedGraph G){
pq=new MinPQ<Edge>();
marked=new boolean[G.V()];
mst=new Queue<>();
visit(G,0);//將起始頂點的所有邊添加到優先隊列中
while(!pq.isEmpty()){
Edge edge = pq.delMin();//取出優先隊列中權值最小的邊
int v=edge.either();//取出權值最小邊的兩個頂點v和w
int w=edge.other(v);
if(marked[v]&&marked[w]){
//如果頂點v和頂點w都已被添加到最小生成樹中,證明該權
//值最小邊已失效,直接進入下一次循環繼續從優先隊列
//中取下一條權值最小邊
continue;
}
mst.enqueue(edge);//將邊添加到最小生成樹中
/** 將頂點(v或w)添加到最小生成樹中,並將與其連接的非失效邊添加到優先隊列中 **/
if(!marked[w]){
visit(G,w);
}
if(!marked[v]){
visit(G,v);
}
}
}
/** 將頂點v添加到最小生成樹中,將連接頂點v的所有非失效邊添加到優先隊列中 **/
private void visit(EdgeWeightedGraph G,int v){
marked[v]=true;
for (Edge edge : G.adj(v)) {
/** 添加非失效邊 **/
if(!marked[edge.other(v)]){
pq.insert(edge);
}
}
}
}
Kruskal算法
Prim算法是一棵樹不斷生長的過程,而Kruskal算法是不斷地將兩棵樹合併直到只剩下一棵樹。
原理
按照邊的權值順序從小到大處理它們,將邊加入到最小生成樹中,要求加入的邊不會與已經加入的邊構成環,直到樹中含有V-1條邊。(V爲頂點總數)
最小生成樹的Kruskal算法實現
package chapter4;
import edu.princeton.cs.algs4.MinPQ;
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.UF;
/**使用了一條隊列來保存最小生成樹的所有邊、
*一條優先隊列來保存還未被檢查的邊和一個union-find的數據結構來判斷是否會產生環
*/
public class KruskalMST {
private Queue<Edge> mst;//最小生成樹隊列
public KruskalMST(EdgeWeightedGraph G){
mst=new Queue<>();
MinPQ<Edge> pq=new MinPQ<>();//最小權值優先隊列
/** 將所有邊插入到優先隊列中 **/
for (Edge edge : G.edges()) {
pq.insert(edge);
}
UF uf = new UF(G.V());//連通圖
while(!pq.isEmpty()&&mst.size()<G.V()-1){
Edge e=pq.delMin();//取出權值最小的邊並得到兩個頂點v和w
int v=e.either();
int w=e.other(v);
if(uf.connected(v,w)){//忽略失效邊,避免構成環
continue;
}
mst.enqueue(e);//將邊添加到最小生成樹中
uf.union(v,w);//合併分量
}
}
/** 返回最小生成樹 **/
public Iterable<Edge> edges(){
return mst;
}
/** 計算最小生成樹權值 **/
public double weight(){
double sumOfWeight=0;
for (Edge edge : mst) {
sumOfWeight+=edge.weight();
}
return sumOfWeight;
}
}