目錄
1、定義
加權圖是一種爲每條邊關聯一個權值或是成本的圖模型。
一幅加權圖的最小生成樹(MST) 是樹中所有邊的權值之和最小 的生成樹。
1.1、約定
在計算圖的最小生成樹的過程中,因爲圖的多種特殊情況,比如負的權值,不連通的情況,會讓我們去做多餘的處理,爲了我們更好的理解最小生成樹的算法, 我們做了下面的約定:
- 只考慮連通圖。如果一幅圖是非連通的, 我們只能使用這個算法來計算它的所有連通分量的最小生成樹,合併在一起稱其爲最小生成 森林。
- 邊的權重不一定表示距離
- 邊的權重可能是 0 或者負數。
- 所有邊的權重都各不相同。如果不同邊的權重可 以相同,最小生成樹就不一定唯一了。
1.2、貪心算法+切分定理
貪心算法(又稱貪婪算法)是指,在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的是在某種意義上的局部最優解。
而我們圖的最小生成樹算法就是利用了貪心算法的原理,我們只要把圖中連接每個點的最小權值的邊找出來,然後並讓他們連成一棵樹,並且不能出現環或者多棵樹我們就算完成了。
而切分定理就是在貪心算法的基礎上,從起點s出發,把起點s和其他點分成兩部分,然後找出起點s和另外一部分連接的最短路徑(也叫橫切邊)。現在就是兩個點,然後找出另外的部分連接這兩個點的最短路徑(橫切邊)連成三個點,這樣不斷持續下去,就能生成我們的最小生成樹。
切分定理:圖的一種切分是將圖的所有頂點分爲兩個非空且不重疊的兩個集合。橫切邊是一條連接 兩個屬於不同集合的頂點的邊。
2、加權無向圖的數據類型
這裏我們求的加權無向圖的最小生成樹,我們首先要表示出加權無向圖的數據類型, 然後我們才能做下一步的計算。
Edge類來存儲邊、邊的權值、邊的兩個頂點。
EdgeWeightedGraph 類 中有一個 數據鏈表,數組用來存儲每個頂點,鏈表用來存儲每個頂點相連的邊。
2.1、Edge類
public class Edge implements Comparable<Edge> {
private final int v;
private final int w;
private final double weight;
public Edge(int v, int w, int 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 return 0; //這裏應該拋出異常
}
@Override
public int compareTo(Edge that) {
if (this.weight>that.weight) return 1;
else if (this.weight<that.weight) return -1;
else return 0;
}
}
2.2、EdgeWeightedGraph
public class EdgeWeightedGraph {
private int V;
private int E;
private Bag<Edge>[] adj;
public EdgeWeightedGraph(int v) {
V = v;
adj = new Bag[v];
for (int i = 0; i < v; i++) {
adj[i] = new Bag<>();
}
}
private void addEdge(Edge e) {
int v = e.either();
int w = e.other(v);
adj[v].add(e);
adj[w].add(e);
E++;
}
public int V() {
return V;
}
public int E() {
return E;
}
public Iterable<Edge> adj(int v) {
return adj[v];
}
public Iterable<Edge> edges() {
Bag<Edge> edges = new Bag<>();
for (int v = 0; v < V; v++) {
for (Edge w : adj(v)) {
if (w.other(v) > v) { //因爲每條邊會被存儲兩次,這個判斷是爲了篩選出一次
edges.add(w);
}
}
}
return edges;
}
}
3、最小生成樹--Prim算法
Prim算法就是在貪心算法的基礎上,從起點s出發,把起點s和其他點分成兩部分,然後找出起點s和另外一部分連接的最短路徑(也叫橫切邊)。現在就是兩個點,然後找出另外的部分連接這兩個點的最短路徑(橫切邊)連成三個點,這樣不斷持續下去,就能生成我們的最小生成樹。
每次連接都會給樹添加一條邊,知道圖中的所有頂點都被連接到樹中。
3.1、Prim算法--方式一
思路就是把所有邊都添加到一個優先隊列中,然後每次從隊列中拿一條權值最小的邊,判斷該邊的兩個頂點是否都連入了樹中,沒有的話就把該邊連入到樹中。直到所有的頂點連接到樹上。
該Prim 算法計算一幅含有 V 個頂點和 E 條邊的連通加權無向圖的最小生成樹 所需的空間與 E 成正比,所需的時間與 ElogE 成正比(最壞情況)。
public class LazyPrimMST {
private boolean[] marked; //判斷是否已經走過該點
private Queue<Edge> mst; //存儲最小生成樹的邊
private MinPQ<Edge> pq; //優先隊列,每次從隊列中取出最短的邊
public LazyPrimMST(EdgeWeightedGraph G) {
marked = new boolean[G.V()];
mst=new Queue<>();
pq = new MinPQ<>();
visit(G,0);
while (!pq.isEmpty()){
Edge e=pq.delMin(); //取出最短的邊
int v=e.either();
int w=e.other(v);
if (marked[v]&&marked[w]) continue;//如果邊的兩個頂點走過了就進行下一次循環
mst.enqueue(e);
if (!marked[v]) visit(G,v); //將沒有放入優先隊列的邊添加進去
if (!marked[w]) visit(G,w);
}
}
private void visit(EdgeWeightedGraph G, int v) {
marked[v] = true;
for (Edge e : G.adj(v)) {
if (!marked[e.other(v)]) {
pq.insert(e);
}
}
}
private Iterable<Edge> mst() {
return mst;
}
}
3.2、Prim算法---方法二
方法二的思路 是從起點s出發,將從s到w點的邊 s--->w 標記爲最短路徑的邊,並存入數組中,當我們在遍歷其他點 v時,v點也能通往w,且邊v--->w比 邊s--->w短, 那麼我們就將數組中通往w的邊替換成v--w。
不斷重複,這樣數組中存放就是通往每個點的最短的邊了,因爲這些邊能通往每個頂點,那麼他們必然也能練成一棵樹,通往起點0是沒有邊的。
該Prim 算法的即計算一幅含有 V 個頂點和 E 條邊的連通加權無向圖的最小生成樹所需的空間和 V 成正比,所需的時間和 ElogV 成正比(最壞情況)。
public class PrimMST {
private boolean[] marked; //判斷是否走過該點
private Edge[] edgeTo; //存放最小生成樹的邊
private double[] distTo; //存放每天邊的權重
private IndexMinPQ<Double> pq; //索引優先隊列,可以通過索引來更改存儲的值
public PrimMST(EdgeWeightedGraph G) {
marked = new boolean[G.V()];
edgeTo = new Edge[G.V()]; //頂點數量和最小生成樹邊的數量一致
distTo = new double[G.V()];
pq = new IndexMinPQ<>(G.V());
for (int i = 0; i < G.V(); i++) { //1、先講每條邊的權值設置爲無窮大
distTo[i] = Double.POSITIVE_INFINITY;
}
distTo[0] = 0.0; //2、起點0 的權值爲0 ,並將起點0加入到優先隊列中
pq.insert(0, 0.0);
while (!pq.isEmpty()) { //3、每次從隊列中取出最短的邊,並返回邊 v-->w 的頂點w
visit(G, pq.delMin());
}
}
private void visit(EdgeWeightedGraph G, int v) {
marked[v] = true; //4、標記走過了該點
for (Edge e : G.adj(v)) { //5、遍歷該點的所有鄰邊
int w = e.other(v);
if (marked[w])
continue;
if (e.weight() < distTo[w]) { //6、如果該邊 v-->w 的權值,比前面加入通往w的邊的權值小,則替換
edgeTo[w] = e;
distTo[w] = e.weight();
if (pq.contains(w)) //7、如果優先隊列已經存了通往w的邊,但不是最短的,則替換
pq.change(w, distTo[w]);
else
pq.insert(w, distTo[w]);
}
}
}
public Iterable<Edge> edges() {
Queue<Edge> mst = new Queue<>();
for (int v = 0; v < this.edgeTo.length; ++v) {
Edge e = this.edgeTo[v];
if (e != null) {
mst.enqueue(e);
}
}
return mst;
}
public double weight() {
double weight = 0.0;
for (Edge e : edges())
weight += e.weight();
return weight;
}
}
4、最小生成樹算法---Kruskal算法
Prim算法的思想是從一個頂點出發不斷地長大成一棵樹。而Kruskal算法的思想是從無數顆小樹不斷合併成一棵大樹。
Kruskal算法的思想是將每條邊都加入優先隊列中,然後每次拿出最小的邊,作爲最小生成樹的一條邊,然後再從中拿出另一條最短的邊,並且這條邊不會和最小生成樹數組中的邊構成環,如果構成環就跳過該條邊,從下一條最短邊開始。這樣就會從無數條短邊開始,不斷合成一棵樹。
Kruskal 算法的計算一幅含有 V 個頂點和 E 條邊的連通加權無向圖的最小生成 樹所需的空間和 E 成正比,所需的時間和 ElogE 成正比(最壞情況)。
public class KruskalMST {
private static final double FLOATING_POINT_EPSILON = 1.0E-12D;
private double weight;
private final Queue<Edge> mst = new Queue<>();
public KruskalMST(EdgeWeightedGraph G) {
MinPQ<Edge> pq = new MinPQ<>();
for (Edge e : G.edges()) {
pq.insert(e);
}
UF uf = new UF(G.V());
while (!pq.isEmpty() && this.mst.size() < G.V() - 1) {
Edge e = (Edge) pq.delMin();
int v = e.either();
int w = e.other(v);
if (uf.find(v) != uf.find(w)) {
uf.union(v, w);
this.mst.enqueue(e);
this.weight += e.weight();
}
}
}
public Iterable<Edge> edges() {
return this.mst;
}
public double weight() {
return this.weight;
}
}