開始討論無向帶權圖。
基本概念
- 最小生成樹:給定一個無向圖,如果該圖的一個生成子圖是一棵樹,則稱該樹爲生成樹(Spanning Tree)。
- 最小生成樹:無向帶權圖中權值之和最小的生成樹,稱之爲最小生成樹(Minimum Spanning Tree)。
- 切分:將圖中的頂點分爲兩部分的劃分,就稱爲一個切分。
- 橫切邊:如果一個邊的兩個端點屬於不同的切分,則稱該邊爲橫切邊。
(G是二分圖存在圖G的一個切分,使得圖中的所有邊都是橫切邊。)
無向帶權圖類的實現
/*Ice_spring 2020/3/26*/
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;
import java.util.TreeSet;
// 圖的鄰接表表示,無向無權圖
public class WeightedGraph implements Cloneable {
private int V; // 頂點數
private int E; // 邊數
private TreeMap<Integer, Integer>[] adj; // 鄰接集合,存放鄰接點和對應邊權值(可以是浮點型)
public WeightedGraph(String filename){
File file = new File(filename);
try(Scanner scanner = new Scanner(file)){
V = scanner.nextInt();
if(V < 0) throw new IllegalArgumentException("V must be a non-neg number!");
adj = new TreeMap[V];
for(int i = 0; i < V; i ++)
adj[i] = new TreeMap<>();
E = scanner.nextInt();
if(E < 0) throw new IllegalArgumentException("E must be a non-neg number!");
for(int i=0; i < E; i ++){
int a = scanner.nextInt();
validateVertex(a);
int b = scanner.nextInt();
validateVertex(b);
int weight = scanner.nextInt();
// 本代碼只處理簡單圖
if(a == b) throw new IllegalArgumentException("檢測到self-loop邊!");
if(adj[a].containsKey(b)) throw new IllegalArgumentException("Parallel Edges are detected!");
adj[a].put(b, weight);
adj[b].put(a, weight);
}
}
catch(IOException e){
e.printStackTrace();//打印異常信息
}
}
public void validateVertex(int v){
// 判斷頂點v是否合法
if(v < 0 ||v >= V)
throw new IllegalArgumentException("vertex " + v + "is invalid!");
}
public int V(){ // 返回頂點數
return V;
}
public int E(){
return E;
}
public boolean hasEdge(int v, int w){
// 頂點 v 到 w 是存在邊
validateVertex(v);
validateVertex(w);
return adj[v].containsKey(w);
}
public Iterable<Integer> adj(int v){
// 返回值可以是TreeSet,不過用 Iterable 接口更能體現面向對象
// 返回和頂點 v 相鄰的所有頂點
validateVertex(v);
return adj[v].keySet();
}
public int getWeight(int v, int w){
// v-w 邊的權值
if(hasEdge(v, w))
return adj[v].get(w);
throw new IllegalArgumentException(String.format("No Edge %d-%d ", v, w));
}
public int degree(int v){
// 返回節點 v 的度,即與該頂點相鄰的頂點個數
validateVertex(v);
return adj[v].size(); //
}
public void removeEdge(int v, int w){
// 刪除 v-w 邊
validateVertex(v);
validateVertex(w);
adj[v].remove(w);
adj[w].remove(v);
}
@Override
public Object clone() {
try {
WeightedGraph cloned = (WeightedGraph) super.clone();
cloned.adj = new TreeMap[V];
for(int v = 0; v < V; v ++){
cloned.adj[v] = new TreeMap<>();
for(Map.Entry<Integer, Integer> entry: adj[v].entrySet())// 遍歷Map的方式
cloned.adj[v].put(entry.getKey(), entry.getValue());
}
return cloned;
}catch (CloneNotSupportedException e){
e.printStackTrace();
}
return null;
}
@Override
public String toString(){
StringBuilder sb = new StringBuilder();
sb.append(String.format("V = %d, E = %d\n",V, E));
for(int v = 0; v < V; v ++){
// 編程好習慣 i,j,k 作索引, v,w 作頂點
sb.append(String.format("%d : ", v));
for(Map.Entry<Integer,Integer>entry: adj[v].entrySet())
sb.append(String.format("(%d: %d)", entry.getKey(), entry.getValue()));
sb.append('\n');
}
return sb.toString();
}
public static void main(String args[]){
WeightedGraph g = new WeightedGraph("g.txt");
System.out.println(g);
}
}
Kruskal算法
Kruskal算法過程
Kruskal算法按權值遞增的次序選擇合適的邊來構造最小生成樹。
對於圖G(V,E),設VT爲生成樹中的頂點,ET爲生成樹中的邊,Kruskal算法具體過程如下:
(1)初始化VT=V,ET=空,即每個頂點看做一顆獨立的樹,此時的T是一個含有V個頂點的森林;
(2)按G的邊的權值遞增順序依次從E-ET中選擇一條邊,若加入這條邊後不構成迴路,則將其加入ET,否則繼續選擇;
(3)重複過程(2)直到ET中有n-1條邊,此時T(VT,ET)就是最小生成樹。
Kruskal算法求解圖的最小生成樹是一種貪心策略,它的正確性可以由切分定理來保證。
切分定理
橫切邊中權值最小的邊一定屬於最小生成樹。
時間複雜度
由於算法的最大時間開銷在邊權值的排序上,故kruskal算法時間複雜度是O(ElogE),因此該算法適合於邊稀疏的圖。
算法的實現
首先給出要存到最小生成樹中的邊類。
public class WeightedEdge implements Comparable<WeightedEdge>{
private int v, w, weight;
public WeightedEdge(int v, int w, int weight){
this.v = v;
this.w = w;
this.weight = weight;
}
public int getV(){return v;}
public int getW(){return w;}
public int getWeight(){return weight;}
@Override
public int compareTo(WeightedEdge another){
// 定義比較
return weight - another.weight;
}
@Override
public String toString(){
return String.format("(%d-%d: %d)", v, w, weight);
}
}
另外爲了判斷新加入的邊是否和已經選擇過的邊構成環,使用並查集來快速動態判斷環。
public class UnionFind {
private int[] parent;
private int[] sz; // sz[i]表示以 i 爲根的集合中元素個數
public UnionFind(int n) {
parent = new int[n];
sz = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i; // 初始每個點都自成一個集合
sz[i] = 1;
}
}
public void unionElements(int p, int q){
// 將兩個元素合併到一個集合, p 連到 q 上,也可以 q 連 p
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
parent[pRoot] = qRoot; // 這裏的pRoot 此後就不會被find到
sz[qRoot] += sz[pRoot];
}
public boolean isConnected(int p, int q){
// 判定p q 是否在一個集合
return find(p) == find(q);
}
public int find(int p) {
// 尋找p 所在的集合
if (p != parent[p])
parent[p] = find(parent[p]);
return parent[p];
}
public int size(int p){
// p 所在集合有幾個元素
return sz[find(p)];
}
}
kruskal算法:
import java.util.ArrayList;
import java.util.Collections;
public class Kruskal {
// 最小生成樹的結果用 ArrayList 保存相應的 n-1 條邊
private WeightedGraph G;
private ArrayList<WeightedEdge> mst;
public Kruskal(WeightedGraph G){
this.G = G;
mst = new ArrayList<>();
CC cc = new CC(G);
if(cc.count() > 1)return;
ArrayList<WeightedEdge> edges = new ArrayList<>();
for(int v = 0; v < G.V(); v ++)
for(int w: G.adj(v))
if(v < w)// 避免 2-0, 0-2 這樣的重複存儲
edges.add(new WeightedEdge(v, w, G.getWeight(v, w)));
Collections.sort(edges);
UnionFind uf = new UnionFind(G.V());
for(WeightedEdge edge: edges){
int v = edge.getV();
int w = edge.getW();
if(!uf.isConnected(v, w)){
mst.add(edge);
uf.unionElements(v, w);
}
}
}
public ArrayList<WeightedEdge> result(){
return mst;
}
public static void main(String args[]){
WeightedGraph g = new WeightedGraph("g.txt");
Kruskal kl = new Kruskal(g);
System.out.println(kl.result());
}
}
Prim算法
Prim算法過程
Prim算法不斷添加新的頂點和權值最小的橫切邊來構造最小生成樹,是切分定理最直接的應用。
對於圖G(V,E),設VT爲生成樹中的頂點,ET爲生成樹中的邊,Prim算法具體過程如下:
(1)初始化VT={u0},ET=空,此時的T是一棵只有一個頂點的空樹;
(2)在G所有的uVT,vV-VT的邊(u,v)中找到一條權值最小的邊(u0,v0)加入集合ET,同時將v0併入集合VT。(該過程就是不斷添加權值最小的橫切邊)
(3)重複過程(2)直到所有頂點都在VT中,此時T(VT,ET)就是最小生成樹。
時間複雜度
經典Prim算法的時間複雜度是O(V^2),適合於求解邊稠密的圖。
算法的實現
import java.util.ArrayList;
import java.util.Collections;
public class Prim {
// 最小生成樹的結果用 ArrayList 保存
private WeightedGraph G;
private ArrayList<WeightedEdge> mst;
public Prim(WeightedGraph G){
this.G = G;
mst = new ArrayList<>();
CC cc = new CC(G);
if(cc.count() > 1)return;
ArrayList<WeightedEdge> edges = new ArrayList<>();
boolean visited[] = new boolean[G.V()];
visited[0] = true;// 初始
for(int i = 1; i < G.V(); i ++){
WeightedEdge minEdge = new WeightedEdge(-1, -1, 0x3f3f3f3f);
for(int v = 0; v < G.V(); v ++)
if(visited[v])
for(int w: G.adj(v))
if(!visited[w] && G.getWeight(v, w) < minEdge.getWeight())
minEdge = new WeightedEdge(v, w, G.getWeight(v, w));
mst.add(minEdge);
visited[minEdge.getV()] = true; // 擴充切分
visited[minEdge.getW()] = true;
}
}
public ArrayList<WeightedEdge> result(){
return mst;
}
public static void main(String args[]){
WeightedGraph g = new WeightedGraph("g.txt");
Prim prim = new Prim(g);
System.out.println(prim.result());
}
}
Prim算法的一個優化
只需要從當前訪問過的頂點的臨邊中選取最小權值的邊而不必要訪問全部的邊,爲此可以使用優先隊列(最小堆)這種數據結構。由於每條邊都會進出優先隊列一次,故優化後的時間複雜度是O(ElogE)。
import java.util.ArrayList;
import java.util.Collections;
import java.util.PriorityQueue;
import java.util.Queue;
public class Prim_Heap {
// 最小生成樹的結果用 ArrayList 保存
private WeightedGraph G;
private ArrayList<WeightedEdge> mst;
public Prim_Heap(WeightedGraph G) {
this.G = G;
mst = new ArrayList<>();
CC cc = new CC(G);
if (cc.count() > 1) return;
ArrayList<WeightedEdge> edges = new ArrayList<>();
boolean visited[] = new boolean[G.V()];
visited[0] = true;// 初始
Queue<WeightedEdge> pq = new PriorityQueue<>();
for(int w: G.adj(0))
pq.add(new WeightedEdge(0, w, G.getWeight(0, w)));
while(!pq.isEmpty()){
WeightedEdge minEdge = pq.remove();
if(visited[minEdge.getV()] && visited[minEdge.getW()])
continue;
mst.add(minEdge);
int newv = visited[minEdge.getV()] == true ? minEdge.getW(): minEdge.getV();
visited[newv] = true;// 拓展切分
for(int w: G.adj(newv))
if(!visited[w])
pq.add(new WeightedEdge(newv, w, G.getWeight(newv, w)));
}
}
public ArrayList<WeightedEdge> result() {
return mst;
}
public static void main(String args[]) {
WeightedGraph g = new WeightedGraph("g.txt");
Prim_Heap prim = new Prim_Heap(g);
System.out.println(prim.result());
}
}