最小生成樹(Minimum Spanning Tree)
生成樹(Spanning Tree),也稱爲支撐樹
- 連通圖的極小連通子圖,它含有圖中全部的 n 個頂點,恰好只有 n – 1 條邊
最小生成樹(Minimum Spanning Tree,簡稱MST)
- 也稱爲最小權重生成樹(Minimum Weight Spanning Tree)、最小支撐樹
- 是所有生成樹中,總權值最小的那棵
- 適用於有權的連通圖(無向)
最小生成樹在許多領域都有重要的作用,例如:
- 要在 n 個城市之間鋪設光纜,使它們都可以通信
- 鋪設光纜的費用很高,且各個城市之間因爲距離不同等因素,鋪設光纜的費用也不同
- 如何使鋪設光纜的總費用最低?—— 最小生成樹的應用
如果圖的每一條邊的權值都互不相同,那麼最小生成樹將只有一個,否則可能會有多個最小生成樹。
求最小生成樹的2個經典算法:
- Prim(普里姆算法)
- Kruskal(克魯斯克爾算法)
Prim算法
切分定理
切分(Cut)
- 把圖中的節點分爲兩部分,稱爲一個切分。
下圖有個切分 C = (S, T),S = { A, B, D },T = { C, E }
橫切邊(Crossing Edge)
- 如果一個邊的兩個頂點,分別屬於切分的兩部分,這個邊稱爲橫切邊。
比如上圖的邊 BC、BE、DE 就是橫切邊
切分定理:給定任意切分,橫切邊中權值最小的邊必然屬於最小生成樹。
Prim算法 – 執行過程
假設 G = (V,E) 是有權的連通圖(無向),A 是 G 中最小生成樹的邊集
- 算法從 S = { u0 }(u0 ∈ V),A = { } 開始,重複執行下述操作,直到 S = V 爲止
找到切分 C = (S,V – S) 的最小橫切邊 (u0,v0) 併入集合 A,同時將 v0 併入集合 S
Prim算法 – 代碼實現
public Set<EdgeInfo<V, E>> mst() {
return prim();
}
private Set<EdgeInfo<V, E>> prim(){
// 最小生成樹的頂點數量爲: 圖的頂點數 1
int verticesSize = vertices.size();
Iterator<Vertex<V, E>> it = vertices.values().iterator(); // map的迭代器
if(!it.hasNext()) return null;
Set<EdgeInfo<V, E>> edgeInfos = new HashSet<>();
Set<Vertex<V, E>> addedVertices = new HashSet<>(); // 標記已經添加的頂點
Vertex<V, E> vertex = it.next(); // 隨機取出一個頂點
addedVertices.add(vertex);
MinHeap<Edge<V, E>> heap = new MinHeap<>(vertex.outEdges, edgeComparator);
while(!heap.isEmpty() && addedVertices.size() < verticesSize){
Edge<V, E> edge = heap.remove();
if(addedVertices.contains(edge.to)) continue;
edgeInfos.add(edge.info());
addedVertices.add(edge.to);
heap.addAll(edge.to.outEdges);
}
return edgeInfos;
}
Kruskal算法
Kruskal算法 – 執行過程
按照邊的權重順序(從小到大)將邊加入生成樹中,直到生成樹中含有 V–1 條邊爲止( V 是頂點數量)
- 若加入該邊會與生成樹形成環,則不加入該邊
- 從第 3 條邊開始,可能會與生成樹形成環
Kruskal算法 – 代碼實現
需要用到並查集數據結構,用來判斷加入的邊是否會形成環。
public Set<EdgeInfo<V, E>> mst() {
return kruskal();
}
private Set<EdgeInfo<V, E>> kruskal(){
// 最小生成樹的頂點數量爲: 圖的頂點數 1
int edgeSize = vertices.size() - 1;
if(edgeSize == -1) return null; // 空的圖
Set<EdgeInfo<V, E>> edgeInfos = new HashSet<>();
MinHeap<Edge<V, E>> heap = new MinHeap<>(edges, edgeComparator);
// 並查集用來判斷加入的邊是否會形成環
UnionFind<Vertex<V, E>> uf = new UnionFind<>();
// 初始化並查集,將其中每個元素設置爲單獨的集合
vertices.forEach((V v, Vertex<V, E> vertex) -> {
uf.makeSet(vertex);
});
while(!heap.isEmpty() && edgeInfos.size() < edgeSize){
Edge<V, E> edge = heap.remove();
if(uf.isSame(edge.from, edge.to)) continue;
edgeInfos.add(edge.info());
uf.union(edge.from, edge.to);
}
return edgeInfos;
}
最短路徑(Shortest Path)
最短路徑是指兩頂點之間權值之和最小的路徑(有向圖、無向圖均適用,不能有負權環)
有向圖:
無向圖:
最短路徑的典型應用之一:路徑規劃問題
求解最短路徑的3個經典算法:
- 單源最短路徑算法
- Dijkstra(迪傑斯特拉算法)
- Bellman-Ford(貝爾曼-福特算法)
- 多源最短路徑算法
- Floyd(弗洛伊德算法)
最短路徑 – 概念
無權圖
無權圖相當於是全部邊權值爲1的有權圖
負權邊
有負權邊,但沒有負權環時,存在最短路徑
A到E的最短路徑是:A → B → E
負權環
有負權環時,不存在最短路徑
通過負權環, A到E的路徑可以無限短
A → E → D → F → E → D → F → E → D → F → E → D → F → E → …
Dijkstra
Dijkstra 屬於單源最短路徑算法,用於計算一個頂點到其他所有頂點的最短路徑
- 使用前提:不能有負權邊
- 時間複雜度:可優化至 O(ElogV) ,E 是邊數量,V 是節點數量
Dijkstra – 等價思考
Dijkstra – 執行過程
Dijkstra – 代碼實現版本1(只返回每條最短路徑的總權值)
接口文件 Graph.java:
package com.mj.graph;
import java.util.List;
import java.util.Map;
import java.util.Set;
public abstract class Graph<V, E> {
protected WeightManager<E> weightManager; // 權重管理
public Graph() {}
public Graph(WeightManager<E> weightManager) {
this.weightManager = weightManager;
}
public abstract int edgesSize(); // 邊的數量
public abstract int verticesSize(); // 頂點數量
public abstract void addVertex(V v); // 添加頂點
public abstract void addEdge(V from, V to); // 添加邊
public abstract void addEdge(V from, V to, E weight);// 添加邊
public abstract void removeVertex(V v); // 刪除頂點
public abstract void removeEdge(V from, V to); // 刪除邊
public abstract void bfs(V begin, VertexVisitor<V> visitor); // 廣度優先搜索
public abstract void dfs(V begin, VertexVisitor<V> visitor); // 深度優先搜索
public abstract List<V> topologicalSort(); // 拓撲排序
public abstract Set<EdgeInfo<V, E>> mst(); // 最小生成樹
public abstract Map<V, E> shortestPath(V begin); // 最短路徑
public interface WeightManager<E> { // 管理權重
int compare(E w1, E w2); // 比較權重
E add(E w1, E w2); // 權重相加
E zero();
}
public interface VertexVisitor<V>{
boolean visit(V v);
}
public static class EdgeInfo<V, E>{
private V from;
private V to;
private E weight;
public EdgeInfo(V from, V to, E weight) { // 邊信息
super();
this.from = from;
this.to = to;
this.weight = weight;
}
public V getFrom() {
return from;
}
public void setFrom(V from) {
this.from = from;
}
public V getTo() {
return to;
}
public void setTo(V to) {
this.to = to;
}
public E getWeight() {
return weight;
}
public void setWeight(E weight) {
this.weight = weight;
}
@Override
public String toString() {
return "EdgeInfo [from=" + from + ", to=" + to + ", weight=" + weight + "]";
}
}
}
@Override
public Map<V, E> shortestPath(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin); // 源點
if(beginVertex == null) return null;
Map<V, E> selectedPaths = new HashMap<>(); // 最終的最短路徑
Map<Vertex<V, E>, E> paths = new HashMap<>(); // 當前最短路徑
// 初始化paths
for(Edge<V, E> edge : beginVertex.outEdges){
paths.put(edge.to, edge.weight);
}
while(!paths.isEmpty()){
Entry<Vertex<V, E>, E> minEntry = getMinPath(paths); // 挑選出當前最短路徑中最短的點
// minVertex 離開桌面,被確定爲最終的最短路徑
Vertex<V, E> minVertex = minEntry.getKey();
selectedPaths.put(minVertex.value, minEntry.getValue());
paths.remove(minVertex);
// 對它的minVertex的outEdges進行鬆弛操作
for(Edge<V, E> edge : minVertex.outEdges){
// 如果edge.to已經離開桌面,就沒必要進行鬆弛操作
if(selectedPaths.containsKey(edge.to.value) || edge.to.equals(beginVertex)) continue;
// 新的可選擇的最短路徑:beginVertex到edge.from的最短路徑 + edge.weight
E newWeight = weightManager.add(minEntry.getValue(), edge.weight);
// 以前的最短路徑:beginVertex到edge.to的最短路徑
E oldWeight = paths.get(edge.to);
if(oldWeight == null || weightManager.compare(newWeight, oldWeight) < 0) {
paths.put(edge.to, newWeight);
}
}
}
return selectedPaths;
}
/**
* 從paths中挑一個最小的路徑出來
*/
private Entry<Vertex<V, E>, E> getMinPath(Map<Vertex<V, E>, E> paths) {
Iterator<Entry<Vertex<V, E>, E>> it = paths.entrySet().iterator();
Entry<Vertex<V, E>, E> minEntry = it.next();
while (it.hasNext()) {
Entry<Vertex<V, E>, E> entry = it.next();
if (weightManager.compare(entry.getValue(), minEntry.getValue()) < 0) {
minEntry = entry;
}
}
return minEntry;
}
Dijkstra – 代碼實現版本2(返回最短路徑的總權值和邊信息)
接口文件 Graph.java
接口文件 Graph.java:修改了 shortestPath
方法接口,增加了 PathInfo
內部類。
package com.mj.graph;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
public abstract class Graph<V, E> {
protected WeightManager<E> weightManager; // 權重管理
public Graph() {}
public Graph(WeightManager<E> weightManager) {
this.weightManager = weightManager;
}
public abstract int edgesSize(); // 邊的數量
public abstract int verticesSize(); // 頂點數量
public abstract void addVertex(V v); // 添加頂點
public abstract void addEdge(V from, V to); // 添加邊
public abstract void addEdge(V from, V to, E weight);// 添加邊
public abstract void removeVertex(V v); // 刪除頂點
public abstract void removeEdge(V from, V to); // 刪除邊
public abstract void bfs(V begin, VertexVisitor<V> visitor); // 廣度優先搜索
public abstract void dfs(V begin, VertexVisitor<V> visitor); // 深度優先搜索
public abstract List<V> topologicalSort(); // 拓撲排序
public abstract Set<EdgeInfo<V, E>> mst(); // 最小生成樹
// public abstract Map<V, E> shortestPath(V begin); // 最短路徑
public abstract Map<V, PathInfo<V, E>> shortestPath(V begin); // 返回路徑信息的最短路徑
public interface WeightManager<E> { // 管理權重
int compare(E w1, E w2); // 比較權重
E add(E w1, E w2); // 權重相加
E zero();
}
public interface VertexVisitor<V>{
boolean visit(V v);
}
/**
* 最短路徑返回的路徑信息, 包含到某個頂點的路徑信息和總權值
*/
public static class PathInfo<V, E> {
protected E weight; // 權值
protected List<EdgeInfo<V, E>> edgeInfos = new LinkedList<>();
public PathInfo() {}
public PathInfo(E weight) {
this.weight = weight;
}
public E getWeight() {
return weight;
}
public void setWeight(E weight) {
this.weight = weight;
}
public List<EdgeInfo<V, E>> getEdgeInfos() {
return edgeInfos;
}
public void setEdgeInfos(List<EdgeInfo<V, E>> edgeInfos) {
this.edgeInfos = edgeInfos;
}
@Override
public String toString() {
return "PathInfo [weight=" + weight + ", edgeInfos=" + edgeInfos + "]";
}
}
public static class EdgeInfo<V, E>{
private V from;
private V to;
private E weight;
public EdgeInfo(V from, V to, E weight) { // 邊信息
super();
this.from = from;
this.to = to;
this.weight = weight;
}
public V getFrom() {
return from;
}
public void setFrom(V from) {
this.from = from;
}
public V getTo() {
return to;
}
public void setTo(V to) {
this.to = to;
}
public E getWeight() {
return weight;
}
public void setWeight(E weight) {
this.weight = weight;
}
@Override
public String toString() {
return "EdgeInfo [from=" + from + ", to=" + to + ", weight=" + weight + "]";
}
}
}
dijkstra 實現1
@Override
public Map<V, PathInfo<V, E>> shortestPath(V begin) {
return dijkstra(begin);
}
private Map<V, PathInfo<V, E>> dijkstra(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin);
if(beginVertex == null) return null;
Map<V, PathInfo<V, E>> selectedPaths = new HashMap<>(); // 最終的最短路徑
Map<Vertex<V, E>, PathInfo<V, E>> paths = new HashMap<>(); // 當前的最短路徑
// 初始化paths
for (Edge<V, E> edge : beginVertex.outEdges) {
PathInfo<V, E> path = new PathInfo<>();
path.weight = edge.weight;
path.edgeInfos.add(edge.info());
paths.put(edge.to, path);
}
while(!paths.isEmpty()) {
// 挑選出當前最短路徑中最短的點
Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = getMinPath(paths);
// minVertex 離開桌面,被確定爲最終的最短路徑
Vertex<V, E> minVertex = minEntry.getKey();
selectedPaths.put(minVertex.value, minEntry.getValue()); // 放入最終的最短路徑
paths.remove(minVertex); // 從當前的最短路徑中移除
// 對離開桌面的minVertex的outEdges進行鬆弛操作
for (Edge<V, E> edge : minVertex.outEdges) {
// 如果edge.to已經離開桌面, 就沒有必要進行鬆弛操作
if(selectedPaths.containsKey(edge.to.value)) continue;
// 新的可選擇的最短路徑: beginVertex到edge.from的最短路徑 + edge.weight
E newWeight = weightManager.add(minEntry.getValue().weight, edge.weight);
// 以前的最短路徑: beginVertex到edge.to的最短路徑
PathInfo<V, E> oldPath = paths.get(edge.to);
if(oldPath == null || weightManager.compare(newWeight, oldPath.weight) < 0) {
PathInfo<V, E> path = new PathInfo<>();
path.weight = newWeight;
path.edgeInfos.addAll(minEntry.getValue().edgeInfos);
path.edgeInfos.add(edge.info());
paths.put(edge.to, path);
}
}
}
selectedPaths.remove(begin);
return selectedPaths;
}
/**
* 從paths中挑一個最小的路徑出來(遍歷)
* 可用小頂堆進行優化
*/
private Entry<Vertex<V, E>, PathInfo<V, E>> getMinPath(Map<Vertex<V, E>, PathInfo<V, E>> paths) {
Iterator<Entry<Vertex<V, E>, PathInfo<V, E>>> it = paths.entrySet().iterator();
Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = it.next();
while (it.hasNext()) {
Entry<Vertex<V, E>, PathInfo<V, E>> entry = it.next();
if (weightManager.compare(entry.getValue().weight, minEntry.getValue().weight) < 0) {
minEntry = entry;
}
}
return minEntry;
}
dijkstra 實現2(封裝了relax鬆弛操作)
private Map<V, PathInfo<V, E>> dijkstra(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin);
if(beginVertex == null) return null;
Map<V, PathInfo<V, E>> selectedPaths = new HashMap<>(); // 最終的最短路徑
Map<Vertex<V, E>, PathInfo<V, E>> paths = new HashMap<>(); // 當前的最短路徑
paths.put(beginVertex, new PathInfo<>(weightManager.zero()));
// 初始化paths
// for (Edge<V, E> edge : beginVertex.outEdges) { // 遍歷源點出去的邊, 添加到當前的最短路徑中
// PathInfo<V, E> path = new PathInfo<>();
// path.weight = edge.weight;
// path.edgeInfos.add(edge.info());
// paths.put(edge.to, path);
// }
while(!paths.isEmpty()) {
// 挑選出當前最短路徑中最短的點
Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = getMinPath(paths);
// minVertex 離開桌面,被確定爲最終的最短路徑
Vertex<V, E> minVertex = minEntry.getKey();
PathInfo<V, E> minPath = minEntry.getValue();
selectedPaths.put(minVertex.value, minPath); // 放入最終的最短路徑
paths.remove(minVertex); // 從當前的最短路徑中移除
// 對離開桌面的minVertex的outEdges進行鬆弛操作
for (Edge<V, E> edge : minVertex.outEdges) {
// 如果edge.to已經離開桌面, 就沒有必要進行鬆弛操作
if(selectedPaths.containsKey(edge.to.value)) continue;
relaxForDijkstra(edge, minPath, paths);
}
}
selectedPaths.remove(begin);
return selectedPaths;
}
/**
* 鬆弛
* @param edge 需要進行鬆弛的邊
* @param fromPath edge的from的最短路徑信息
* @param paths 存放着其他(對於dijkstra來說, 就是還沒有離開桌面的點)的最短路徑信息
*/
private void relaxForDijkstra(Edge<V, E> edge, PathInfo<V, E> fromPath, Map<Vertex<V, E>, PathInfo<V, E>> paths) {
// 新的可選擇的最短路徑: beginVertex到edge.from的最短路徑 + edge.weight
E newWeight = weightManager.add(fromPath.weight, edge.weight);
// 以前的最短路徑: beginVertex到edge.to的最短路徑
PathInfo<V, E> oldPath = paths.get(edge.to);
if(oldPath != null && weightManager.compare(newWeight, oldPath.weight) >= 0) return;
if(oldPath == null) { // 新創建的邊
oldPath = new PathInfo<>();
paths.put(edge.to, oldPath);
} else { // 以前就存在的邊
oldPath.edgeInfos.clear();
}
oldPath.weight = newWeight;
oldPath.edgeInfos.addAll(fromPath.edgeInfos);
oldPath.edgeInfos.add(edge.info());
}
/**
* 從paths中挑一個最小的路徑出來(遍歷)
* 可用小頂堆進行優化
*/
private Entry<Vertex<V, E>, PathInfo<V, E>> getMinPath(Map<Vertex<V, E>, PathInfo<V, E>> paths) {
Iterator<Entry<Vertex<V, E>, PathInfo<V, E>>> it = paths.entrySet().iterator();
Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = it.next();
while (it.hasNext()) {
Entry<Vertex<V, E>, PathInfo<V, E>> entry = it.next();
if (weightManager.compare(entry.getValue().weight, minEntry.getValue().weight) < 0) {
minEntry = entry;
}
}
return minEntry;
}
dijkstra 測試
運行:
Bellman-Ford
Bellman-Ford 也屬於單源最短路徑算法,支持負權邊,還能檢測出是否有負權環
- 算法原理:對所有的邊進行 V – 1 次鬆弛操作( V 是節點數量),得到所有可能的最短路徑
- 時間複雜度:O(EV) ,E 是邊數量,V 是節點數量
下圖的最好情況是恰好從左到右的順序對邊進行鬆弛操作:
- 對所有邊僅需進行 1 次鬆弛操作就能計算出A到達其他所有頂點的最短路徑
最壞情況是恰好每次都從右到左的順序對邊進行鬆弛操作:
- 對所有邊需進行 V – 1 次鬆弛操作才能計算出A到達其他所有頂點的最短路徑
Bellman-Ford – 實例
Bellman-Ford – 代碼實現
@Override
public Map<V, PathInfo<V, E>> shortestPath(V begin) {
// return dijkstra(begin);
return bellmanFord(begin);
}
private Map<V, PathInfo<V, E>> bellmanFord(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin); // 源點
if(beginVertex == null) return null;
// 存放當前最短路徑信息(不斷的進行鬆弛操作, 會變成最終的最短路徑)
Map<V, PathInfo<V, E>> selectedPaths = new HashMap<>();
// 初始化源點的最短路徑信息
selectedPaths.put(begin, new PathInfo<>(weightManager.zero()));
int count = vertices.size() - 1; // 進行 V -1 次鬆弛操作, 必然能找到最短路徑
for (int i = 0; i < count; i++) {
for (Edge<V, E> edge : edges) { // 對所有邊進行鬆弛操作
// 獲取該邊的始點的最短路徑信息, 用於後面進行鬆弛操作
PathInfo<V, E> fromPath = selectedPaths.get(edge.from.value);
// 如果該點的始點沒有最短路徑信息,鬆弛必然失敗,直接進入下一輪
if(fromPath == null) continue;
relax(edge, fromPath, selectedPaths); // 鬆弛操作
}
}
// 檢測負權環, 前面已經鬆弛了V-1次,這裏如果鬆弛第 V 次仍然可以成功, 說明有負權環
for (Edge<V, E> edge : edges) {
PathInfo<V, E> fromPath = selectedPaths.get(edge.from.value);
if(fromPath == null) continue;
if(relax(edge, fromPath, selectedPaths)) {
System.out.println("有負權環, 不存在最短路徑");
return null;
}
}
selectedPaths.remove(begin); // 從最短路徑中移除源點的最短路徑信息
return selectedPaths;
}
/**
* 鬆弛
* @param edge 需要進行鬆弛的邊
* @param fromPath edge的from的最短路徑信息
* @param paths 存放着其他的最短路徑信息
*/
private boolean relax(Edge<V, E> edge, PathInfo<V, E> fromPath, Map<V, PathInfo<V, E>> paths) {
// 新的可選擇的最短路徑: beginVertex到edge.from的最短路徑 + edge.weight
E newWeight = weightManager.add(fromPath.weight, edge.weight);
// 以前的最短路徑: beginVertex到edge.to的最短路徑
PathInfo<V, E> oldPath = paths.get(edge.to.value);
if(oldPath != null && weightManager.compare(newWeight, oldPath.weight) >= 0) return false;
if(oldPath == null) { // 新創建的邊
oldPath = new PathInfo<>();
paths.put(edge.to.value, oldPath);
} else { // 以前就存在的邊
oldPath.edgeInfos.clear();
}
oldPath.weight = newWeight;
oldPath.edgeInfos.addAll(fromPath.edgeInfos);
oldPath.edgeInfos.add(edge.info());
return true;
}
/**
* 從paths中挑一個最小的路徑出來(遍歷)
* 可用小頂堆進行優化
*/
private Entry<Vertex<V, E>, PathInfo<V, E>> getMinPath(Map<Vertex<V, E>, PathInfo<V, E>> paths) {
Iterator<Entry<Vertex<V, E>, PathInfo<V, E>>> it = paths.entrySet().iterator();
Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = it.next();
while (it.hasNext()) {
Entry<Vertex<V, E>, PathInfo<V, E>> entry = it.next();
if (weightManager.compare(entry.getValue().weight, minEntry.getValue().weight) < 0) {
minEntry = entry;
}
}
return minEntry;
}
測試
Floyd
Floyd 屬於多源最短路徑算法,能夠求出任意2個頂點之間的最短路徑,支持負權邊
- 時間複雜度:O(V3),效率比執行 V 次 Dijkstra 算法要好( V 是頂點數量)
注:單源最短路徑算法對每個頂點求一次,同樣可以求出任意2個頂點之間的最短路徑
算法原理:
- 從任意頂點
i
到任意頂點j
的最短路徑不外乎兩種可能
① 直接從i
到j
② 從i
經過若干個頂點到j
- 假設
dist(i, j)
爲頂點i
到頂點j
的最短路徑的距離 - 對於每一個頂點
k
,檢查dist(i, k)
+dist(k, j)
<dist(i, j)
是否成立
如果成立,證明從i
到k
再到j
的路徑比i
直接到j
的路徑短,
設置dist(i, j)
=dist(i, k)
+dist(k, j)
; - 當我們遍歷完所有結點
k
,dist(i, j)
中記錄的便是i
到j
的最短路徑的距離
算法原理僞代碼: