本文為原創文章,轉載請注明出處
查看[數據結構及算法]系列內容請點擊:http://www.lxweimin.com/nb/45938463
本文通過一個例子及其變種,由淺入深解析常見的圖搜索算法。
例子如下:
假設有如下矩陣:
S表示開始節點,E表示結束節點
目標:假如一個人站在圖中的S方格,他一次可以往上下左右方向移動一格,但不能走到黑色的方格上,計算從S(Start)方格到E(End)方格最少需要走幾格?
這個問題及其變種有很多種解決方案,下面我們說【方格】或者說【點】指的都是方格,下面就從這個問題開始一一介紹這些算法。
DFS:深度優先搜索算法
最簡單(但不一定最適合)的方法就是深度優先搜索算法,深度優先一般使用遞歸策略,可以理解為窮舉從S
到E
的所有路徑,然后找到一個最短的路徑即可。需要注意的是,在一次搜索路徑中,已經被走過的方格不能再次走上去。
假設我們搜索的方法是從S
的12點方向順時針搜索,即遵循先找最上面相鄰的點,其次找右上角,再次右邊的點...
如此往復... 那么第一次從S
搜索到E
的路徑如下所示:
第一次搜索完成后記錄下這個路徑的長度,然后繼續從
E
點退回到上一個點,繼續搜索其他方向,如下圖綠色箭頭所示:
其中,灰色箭頭表示回退到上一個點,綠色箭頭表示繼續搜索的路徑。如此往復直到窮舉完所有的路徑為止,用代碼表示如下:
public class MainTest {
// 定義移動方向
private static final int[][] directions = new int[][]{{-1, 0}, {-1, 1}, {0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}, {-1, -1}};
private static int allPathCount = 0; // 記錄下所有走法的個數
public static void main(String[] args) {
int[][] g = new int[][]{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
};
System.out.println(dfs(g, 2, 1, 2, 4)); // 輸出 4
System.out.println("共有:" + allPathCount + "種走法"); // 共有:163506種走法
}
/**
* g 表示初始化的圖,這里用0表示空白方格,1表示不可走方格,在程序里面,我們會用-1表示已經走過的方格
* 這里為了方便表示,使用行列來表示,而不是使用x、y來表示
* (startLine, startColumn)和(endLine, endColumn) 分別表示起始和終止方格的行列
*/
public static int dfs(int[][] g, int startLine, int startColumn, int endLine, int endColumn) {
if (startLine == endLine && startColumn == endColumn) { // 到達E點
allPathCount++;
return 0;
}
g[startLine][startColumn] = -1; // 標記為已經走過
int minStep = Integer.MAX_VALUE / 2;
for (int[] direct : directions) {
int newLine = startLine + direct[0];
int newColumn = startColumn + direct[1];
// 判斷不可走的格子
if (newLine < 0 || newLine >= g.length || newColumn < 0 || newColumn >= g[0].length || g[newLine][newColumn] != 0)
continue;
int step = 1 + dfs(g, newLine, newColumn, endLine, endColumn); // 遞歸查找
if (step < minStep) minStep = step;
}
g[startLine][startColumn] = 0; // 釋放標記,讓其他路線可走
return minStep;
}
}
可以看到,上述結束輸出為4,實際上走的步數為4的路線為:
可以看到,走法一共有163506種,我們實際上需要將這么多種走法全部窮舉完,才可以計算出來走的步數的最小值,當然,在實際運用過程中,DFS可以在運行過程中對其搜索樹進行剪枝(比如對于當前步數已經大于全局到達目標點步數的最小值了就沒必要繼續往下搜索了),可以很大程度上減少不必要的重復。
根據上面DFS的走法記錄,我們看到,基本思想是使用遞歸構建一個隱式的搜索樹,然后窮舉樹的深度從而取出最小深度,其所要求的時間復雜度較高。時間復雜度分析如下:
- 第一個點具有8個方向,第二個點具有7個方向,那么整體需要搜索的次數約為:
8×7×7×...×7
,具有多少個7呢?我們分析,搜索樹的深度最多為n × m
,n
和m
分別為矩陣行列數。總搜索節點數大約為8 ×7^(n×m)
,所以總體上時間復雜度為O(8^(n×m))
,如果只能向上下左右4個方向移動,那么時間復雜度為O(4^(n×m))
。總之,在最壞情況下,時間復雜度是指數級的。- 如果我們不關注最短的步數,而是隨機找出一條路徑能夠到達終點,那么,時間復雜度為
O(V+E)
,其中,V
是圖中邊的個數,E
是圖中點的個數。- 使用遞歸的話,空間復雜度與時間復雜度相等,使用棧來解決的話,空間復雜度為
O(n×m)
BFS:廣度優先搜索算法
對于簡單的尋路問題,我們也可以使用廣度優先搜素算法解決,從而大大縮小其時間復雜度。BFS的基本思想是每次從當前節點擴展一層,每次遍歷子節點的時候,依次將子節點往外擴展一層,由于這樣的話步數最小的步驟總是在前面,所以當某一次擴展擴展到了E
點就可以結束了。同時,由于后擴展的節點步長肯定比前面擴展的步長更長,所以可以設置全局訪問過的節點不再繼續訪問。基本擴展步驟如下:
如圖所示,第一次擴展使用紅色箭頭表示,擴展其相鄰方哥,第二次擴展用綠色箭頭表示...如此往復,等到第四次擴展訪問到了
E
點,整個算法就結束了,代碼如下:
public class MainTest {
// 定義移動方向
private static final int[][] directions = new int[][]{{-1, 0}, {-1, 1}, {0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}, {-1, -1}};
public static void main(String[] args) {
int[][] g = new int[][]{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
};
System.out.println(bfs(g, 2, 1, 2, 4)); // 輸出 4
}
/**
* g 表示初始化的圖,這里用0表示空白方格,1表示不可走方格,在程序里面,我們會用-1表示已經走過的方格
* 這里為了方便表示,使用行列來表示,而不是使用x、y來表示
* (startLine, startColumn)和(endLine, endColumn) 分別表示起始和終止方格的行列
*/
public static int bfs(int[][] g, int startLine, int startColumn, int endLine, int endColumn) {
Triple[] path = new Triple[g.length * g[0].length]; // 這里實際用Queue隊列更方便
int head = 1, tail = 0;
path[0] = new Triple(startLine, startColumn, 0); // 初始點入隊列
g[startLine][startColumn] = -1; // 標記初始點不可走
while (head > tail) {
Triple t = path[tail];
for (int i = 0; i < directions.length; i++) {
int newLine = t.line + directions[i][0];
int newColumn = t.column + directions[i][1];
// 找到直接返回
if (newLine == endLine && newColumn == endColumn) return t.step + 1;
if (newLine < 0 || newLine >= g.length || newColumn < 0 || newColumn >= g[0].length || g[newLine][newColumn] != 0)
continue;
g[newLine][newColumn] = -1; // 標記為已走過
path[head++] = new Triple(newLine, newColumn, t.step + 1); // 繼續將子節點入隊列
}
tail++;
}
return -1;
}
private static class Triple {
int line;
int column;
int step;
public Triple(int line, int column, int step) {
this.line = line;
this.column = column;
this.step = step;
}
}
}
后面很多的算法都借鑒了BFS算法,并進行了一定的變化。
- 使用邊和點來表示的話,時間復雜度為
O(V+E)
,其中V
是點的個數,E
是邊的個數,上面的例子里面,邊的個數約有n×m×8
個(忽略邊緣和不可走的點周邊的邊不到8個的情況),所以時間復雜度為:O(n×m×9)
A*算法:啟發式尋路
下面我們把上面的題目變換一下:
假設還是上面的圖,從任意方格橫向或者豎向走,所需要走的步數為2,斜向走需要走的步數為3,請問最少需要多少步數能從
S
點走到E
點?(后續將步數也稱為代價)
很明顯,這里就是一個帶權的圖搜索問題了。當我們可以大概評估從任意一格走到終點E
的代價的時候,就可以使用A算法,A算法實際上是一種貪心算法,由Dijkstra算法改進而來,下面詳細介紹。
A*算法基本元素
- 在A*算法中,需要維護兩個列表,一個是open list列表,用來存儲在下一次搜索中可能會被搜索到的點,open list是一個按照后面說的
F
值從小到大排好序的list,open list一開始只有一個起點。 - 第二個列表是close list列表,已經檢測過的節點放入close list,在close list中的點不會再次被檢測。
- 父節點,記錄回溯的節點,如果只是找出最短路徑的長度,不進行回溯的話,則不需要父節點。
- 路徑排序,
F
值,F = G + H
,其中,G
為從起點到當前節點的代價,H
為從當前節點到終點的估算代價,即:啟發函數。 - 啟發函數,即
H
,用來估算從當前節點到終點的代價,啟發函數的選擇好壞會直接影響算法的性能與結果。這里就使用曼哈頓距離,即橫縱向走的代價之和:H=|(iS-iK)|×2 + |(jS-jK)|×2,2是走一格的代價。
算法流程
- 1、把起點
S
加入到open list中,并估算S
點的F
值; - 2、從open list中取出一個
F
值最小的點P
,向周圍一格(橫豎向或斜向)擴展點,并遵循3、4、5規則; - 3、若周圍的某個格子不可走或在close list中,則忽略;
- 4、若周圍的某個格子不在open list或close list中,且可走,則估算其F值,并按照F值從小到大將該格子加入open list;
- 5、若周圍某個格子在open list中,且該節點的
F
值大于:P
點的F
值+該節點的H
值+P
到該節點的代價,則更新該節點的F
值,并將該節點的父節點設置為P
點。 - 6、重復步驟2,直到找到
S
點為止。
按照以上步驟,我們可以看到第一次的搜索路徑如下(對于具有相同的F
值的點,隨便選取一個擴展即可):
第二次搜索擴展如下:
經過多輪擴展后的最終結果:
最終計算,最先到達E
點的路徑F=11
,這里F值就代表其最終代價。
可以看到,H
函數的選擇對于評估具有非常重要的作用更高,H
函數會直接影響最終結果。
A*算法實現的代碼如下:
import java.util.*;
public class MainTest {
// 定義移動方向
private static final int[][] directions = new int[][]{{-1, 0}, {-1, 1}, {0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}, {-1, -1}};
private static final int[] walkStep = new int[]{2, 3, 2, 3, 2, 3, 2, 3}; // 八個方向走一步的代價
public static void main(String[] args) {
int[][] g = new int[][]{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
};
System.out.println(aStar(g, 2, 1, 2, 4)); // 輸出 11
}
public static int aStar(int[][] g, int startLine, int startColumn, int endLine, int endColumn) {
List<Point> openList = new ArrayList<>(); // close list中的節點我們直接將其值置為-1,不再單獨開list保存,在open list中的點標記為2
Point[][] pointIndex = new Point[g.length][g[0].length]; // 用于加快從(line, column)查找Point的效率
openList.add(new Point(startLine, startColumn, 0, endLine, endColumn));
g[startLine][startColumn] = 2; // 這一行不是必須的,只是為了讓邏輯更清晰
pointIndex[startLine][startColumn] = openList.get(0);
while (openList.size() > 0) {
Point p = openList.remove(0);
g[p.line][p.column] = -1;
for (int i = 0; i < directions.length; i++) {
int newLine = p.line + directions[i][0];
int newColumn = p.column + directions[i][1];
// 找到直接返回
if (newLine == endLine && newColumn == endColumn) return p.G + walkStep[i];
// 不可走或已經在close list中,則忽略
if (newLine < 0 || newLine >= g.length || newColumn < 0 || newColumn >= g[0].length || (g[newLine][newColumn] != 0 && g[newLine][newColumn] != 2))
continue;
// 若沒有在open list中則加入
if (g[newLine][newColumn] != 2) {
g[newLine][newColumn] = 2;
Point p1 = new Point(newLine, newColumn, p.G + walkStep[i], endLine, endColumn);
insertOpenList(openList, p1);
pointIndex[newLine][newColumn] = p1;
} else { // 若在open list中,則根據情況更新
int newF = p.G + walkStep[i] + Math.abs((endLine - newLine) * 2) + Math.abs((endColumn - newColumn) * 2);
if (newF < pointIndex[newLine][newColumn].F) {
Point p1 = new Point(newLine, newColumn, p.G + walkStep[i], endLine, endColumn);
openList.remove(pointIndex[newLine][newColumn]);
insertOpenList(openList, p1);
pointIndex[newLine][newColumn] = p1;
}
}
}
}
return -1;
}
// 插入列表
public static void insertOpenList(List<Point> openList, Point p) {
int insertIndex = Collections.binarySearch(openList, p, (a, b) -> a.F - b.F); // 二分查找需要插入的地方
if (insertIndex < 0) insertIndex = -(insertIndex + 1);
openList.add(insertIndex, p);
}
private static class Point {
int line;
int column;
int F;
int G;
int H;
// 自動計算F值
public Point(int line, int column, int G, int endLine, int endColumn) {
this.line = line;
this.column = column;
this.G = G;
this.H = Math.abs((endLine - line) * 2) + Math.abs((endColumn - column) * 2);
this.F = this.G + this.H;
}
}
}
A*算法的時間復雜度不是很好估計,但是可以初步估計其 空間復雜度為
O(V)
量級,時間復雜度取決于H
函數的計算策略、排序方法等,我們這里的H
函數復雜度為O(1)
,排序算法為O(log(V))
,對于每一個需要插入的點都需要一次或多次排序,在極限情況下可以認為對檢查每條邊都需要進行一次排序,那么排序次數為E
次,點的個數為V
,邊的個數為E
,那么按照排序來算,時間復雜度為O(E×log(V))
Dijkstra算法
剛剛在A*算法中,我們認為上下左右走的時候,其步數為2,斜向走的時候步數為3,實際上就是我們為邊進行了加權,為了更加方便的討論,我們從這里開始,將上面的矩陣進行簡化,簡化為圖的形式,并進行一部分點的簡化和邊的加權簡化。
簡化后的圖 如下所示:
如上圖所示,一共有6個點,點之間可以互相連接,也可以不互相連接,箭頭上的數字代表了從一個點到另外一個點所需要走的步數(代價),試求從
S
點到E
點所需要的最小代價。
Dijkstra算法屬于典型的貪心算法,算法思想是記錄從S
點到達每一個點的當前最短路徑,然后不斷通過“松弛”操作來進行調整最短路徑長度。算法的基本步驟如下:
- 1、初始化一個列表
L
,記錄其余的各個點和S
到達他們的距離,不能直達的則標記距離為無窮大,對于L
中能直達的點集合,我們標記為L1
,不能直達的點集合標記為L2
,所以:L = L1 ∪ L2
,然后進行第2步的松弛操作;- 2、從
L2
中取出一個點P
,點P
需要滿足條件:P
是L2
中距離L1
中所有的點距離最近的點。然后將P
加入L1
并計算最短的距離,如果L2
中的某點Q
距離S
的距離滿足:distance(S -> Q) 大于 distance(s -> P ->Q)
,那么更新其距離為distance(s -> P ->Q)
。- 3、重復操作2,直到所有
L2
為空,或者L2
中的點不能加入到L1
(非連通圖)為止。
按照上述步驟模擬,最終會找出從S
到E
的最短路徑為:
所需的步數最小為14。
Java實現的代碼如下:
import java.util.*;
public class MainTest {
public static void main(String[] args) {
// 初始化圖
Point S = new Point('S');
Point p1 = new Point('p');
Point p2 = new Point('p');
Point p3 = new Point('p');
Point p4 = new Point('p');
Point E = new Point('E');
S.addChild(p1, 3);
S.addChild(p2, 12);
p2.addChild(p1, 5);
p1.addChild(p3, 8);
p3.addChild(p4, 1);
p2.addChild(p4, 30);
p3.addChild(E, 10);
p4.addChild(E, 2);
System.out.println(dijkstra(S, E)); // 輸出 14
}
public static int dijkstra(Point S, Point E) {
Map<Point, Integer> L1 = new HashMap<>(); // L1列表,為了加快效率這里使用HashMap, key=Point value=length
L1.put(S, 0);
while (true) {
Point P = null;
int minLen = Integer.MAX_VALUE / 2;
// 尋找L1中的點的所有子節點
for (Point Q : L1.keySet()) {
int existWeight = L1.get(Q);
for (int i = 0; i < Q.children.size(); i++) {
if (L1.containsKey(Q.children.get(i))) continue; // 在L1列表中的忽略
if (existWeight + Q.weight.get(i) < minLen) {
minLen = existWeight + Q.weight.get(i);
P = Q.children.get(i);
break; // 這里是一個小優化,插入的時候把權重最小的放在前面,這里可以直接break
}
}
}
if (P == null) break;
if (P == E) return minLen;
L1.put(P, minLen);
// TODO 這里由于上面是兩層循環所以沒有做松弛操作
}
return -1;
}
// 定義節點類型
private static class Point {
char name;
List<Point> children = new ArrayList<>(); // 子節點
List<Integer> weight = new ArrayList<>(); // 邊的權重
Point(char n) {
this.name = n;
}
// 插入的時候注意,權重最小的放在最前面
void addChild(Point p, int w) {
int insertIndex = Collections.binarySearch(weight, w, (a, b) -> a - b); // 二分法查找插入index
if (insertIndex < 0) insertIndex = -(insertIndex + 1);
children.add(insertIndex, p);
weight.add(insertIndex, w);
}
}
}
可以看到,實際上Dijkstra算法在計算的過程中,在找到目標節點的時候不返回,那么就求出了從S
點到其他任意一點的最短距離。所以該算法比較適合用來求從某個特定的點到另外其他的所有點的最短距離。
Dijkstra算法的時間復雜度,內層的
for
循環需要循環K
次,K = 1 + 2 + 3 + ... + (V-1) = (V^2) / 2
,總體時間復雜度為O(V^2)
。這里為了加快搜索速度,使用了一個HashMap來存儲Point,所以空間復雜度為O(V)
Floyd算法:尋找圖中任意兩點的最短距離
我們將上面題目變一下:
請輸出圖中任意兩點之間的距離。
最簡單的辦法是對每個節點都跑一次Dijkstra算法,時間復雜度為O(V^3)
,這里我們不介紹這種方法。我們主要介紹Floyd算法。
Floyd算法是一種使用動態規劃思想來進行計算路徑的算法。對于圖中的任意三個點i, j, k
,我們用distance(i,j)
表示從i
到j
的距離,用distance(i, k, j)
表示i
經過k
再到j
的距離,主要思路是:
if distance(A, B) > distance(A, C, B) then: distance(A, B) = distance(A, C, B)
Floyd算法一般為了好計算,使用鄰接矩陣來進行表示圖。
如下圖,用鄰接矩陣對每個點都進行編號,表示如下:
算法基本思路:
每次選取一個中間點
k
,窮舉i
和j
點,如果distance(i,j) > distance(i,k,j)
則更新distance(i,j)=distance(i,k,j)
代碼實現:
public class MainTest {
private static int X = Integer.MAX_VALUE / 2; // 表示無窮大
public static void main(String[] args) {
int[][] g = new int[][]{
{0, 3, X, 12, X, X},
{X, 0, 8, X, X, X},
{X, X, 0, X, 1, 10},
{X, 5, X, 0, 30, X},
{X, X, X, X, 0, 2},
{X, X, X, X, X, 0}
};
int[][] distance = floyd(g);
for (int i = 0; i < distance.length; i++) {
for (int j = 0; j < distance[i].length; j++)
System.out.println("" + i + " -> " + j + " : " + distance[i][j]);
}
}
public static int[][] floyd(int[][] g) {
int[][] distance = g; // 這里偷個懶,直接用圖的原始權重表示距離
for (int k = 0; k < g.length; k++) {
for (int i = 0; i < g.length; i++) {
for (int j = 0; j < g.length; j++) {
if (i == j) continue;
if (distance[i][j] > distance[i][k] + distance[k][j])
distance[i][j] = distance[i][k] + distance[k][j];
}
}
}
return distance;
}
}
輸出結果:
0 -> 0 : 0
0 -> 1 : 3
0 -> 2 : 11
0 -> 3 : 12
0 -> 4 : 12
0 -> 5 : 14
1 -> 0 : 1073741823
1 -> 1 : 0
1 -> 2 : 8
1 -> 3 : 1073741823
1 -> 4 : 9
1 -> 5 : 11
2 -> 0 : 1073741823
2 -> 1 : 1073741823
2 -> 2 : 0
2 -> 3 : 1073741823
2 -> 4 : 1
2 -> 5 : 3
3 -> 0 : 1073741823
3 -> 1 : 5
3 -> 2 : 13
3 -> 3 : 0
3 -> 4 : 14
3 -> 5 : 16
4 -> 0 : 1073741823
4 -> 1 : 1073741823
4 -> 2 : 1073741823
4 -> 3 : 1073741823
4 -> 4 : 0
4 -> 5 : 2
5 -> 0 : 1073741823
5 -> 1 : 1073741823
5 -> 2 : 1073741823
5 -> 3 : 1073741823
5 -> 4 : 1073741823
5 -> 5 : 0
可以看到,輸出數字特別大的就是從i
到j
不可到達的,我們看0 -> 5 : 14
與上面Dijkstra計算的結果一致。
Floyd算法時間復雜度為
O(V^3)
,由于一般需要(我們的代碼里面并沒有)有一個數組來存儲計算結果,所以空間復雜度為O(V)
Bellman-Form算法:解決帶負權回路的最短路徑算法
我們將上面Dijkstra算法用到的圖添加一條邊:
從
4
到3
多了一條權重為-15的邊,這時候再求從S
到其他所有點的最短距離,我們就能明顯發現一個問題:
在進行松弛操作的時候,假如當前
L1
列表中包含了S, 1, 2, 4
點,當想納入3
點的時候,會發現,經過3
到達1
點更近,從S
到達1
點直接距離為3,經過3
點后從S
到達1
點的距離變為了2,同時也會涉及到2
點的距離更新。而我們在Dijkstra中并沒有這種更新機制。
實際上,對于上面的情況,我們無法求出S
點到達1, 2, 3, 4, 5
點的最短路徑,或者最短路徑為負無窮大。Bellman-Form算法就是在Dijkstra的基礎上進行了負權環狀回路的檢測,檢測到這種情況就返回并告知無法求出。
負權回路實際上指的是回路上所有權重的最小值相加后的值是負的,比如上面的圖,如果點4
到點3
的權重為-5,那么就不會存在問題。
那么如何檢測負權回路呢?這里給一個公式:
如果松弛完成以后還存在
distance(v) > distance(u) + w(u,v)
,那么就存在負權回路。帶入上圖,v
就是點2
,u
就是點1
首先,我們拿Dijkstra算法來改一下:
import java.util.*;
public class MainTest {
public static void main(String[] args) {
// 初始化圖
Point S = new Point('S');
Point p1 = new Point('p');
Point p2 = new Point('p');
Point p3 = new Point('p');
Point p4 = new Point('p');
Point E = new Point('E');
S.addChild(p1, 3);
S.addChild(p2, 12);
p2.addChild(p1, 5);
p1.addChild(p3, 8);
p3.addChild(p4, 1);
p2.addChild(p4, 30);
p3.addChild(E, 10);
p4.addChild(E, 2);
p4.addChild(p2, -15); // 添加一個權重為-15的邊
System.out.println(dijkstra(S, 6)); //輸出 null
}
// 這里改成了返回從`S`點到到其他所有點的最短路徑
public static Map<Point, Integer> dijkstra(Point S, int pointCount) {
Map<Point, Integer> L1 = new HashMap<>(); // L1列表,為了加快效率這里使用HashMap, key=Point value=length
L1.put(S, 0);
while (true) {
Point P = null;
int minLen = Integer.MAX_VALUE / 2;
// 尋找L1中的點的所有子節點
for (Point Q : L1.keySet()) {
int existWeight = L1.get(Q);
for (int i = 0; i < Q.children.size(); i++) {
if (L1.containsKey(Q.children.get(i))) continue; // 在L1列表中的忽略
if (existWeight + Q.weight.get(i) < minLen) {
minLen = existWeight + Q.weight.get(i);
P = Q.children.get(i);
break; // 這里是一個小優化,插入的時候把權重最小的放在前面,這里可以直接break
}
}
}
if (P == null) break;
L1.put(P, minLen);
}
// 如果有的點不可達,返回null
if (pointCount != L1.size()) return null;
// 檢測負權回路
for (Point v : L1.keySet()) {
int distanceV = L1.get(v);
for (Point u : L1.keySet()) {
int distanceU = L1.get(u);
int w = Integer.MAX_VALUE / 2;
if (u.children.contains(v)) w = u.weight.get(u.children.indexOf(v));
if (distanceV > distanceU + w) return null;
}
}
return L1;
}
// 定義節點類型
private static class Point {
char name;
List<Point> children = new ArrayList<>(); // 子節點
List<Integer> weight = new ArrayList<>(); // 邊的權重
Point(char n) {
this.name = n;
}
// 插入的時候注意,權重最小的放在最前面
void addChild(Point p, int w) {
int insertIndex = Collections.binarySearch(weight, w, (a, b) -> a - b); // 二分法查找插入index
if (insertIndex < 0) insertIndex = -(insertIndex + 1);
children.add(insertIndex, p);
weight.add(insertIndex, w);
}
}
}
如果可以不用Dijkstra算法,而定義另外一種松弛操作:
// w[j -> i]表示從j直接到i的權重
if distance[i] > distance[j] + w[j -> i] then distance[i] = distance[j] + w[j -> i]
代碼如下:
import java.util.*;
public class MainTest {
public static void main(String[] args) {
// 初始化圖
Point S = new Point('S');
Point p1 = new Point('p');
Point p2 = new Point('p');
Point p3 = new Point('p');
Point p4 = new Point('p');
Point E = new Point('E');
S.addChild(p1, 3);
S.addChild(p2, 12);
p2.addChild(p1, 5);
p1.addChild(p3, 8);
p3.addChild(p4, 1);
p2.addChild(p4, 30);
p3.addChild(E, 10);
p4.addChild(E, 2);
p4.addChild(p2, -15); // 添加一個權重為-15的邊
List<Point> points = Arrays.asList(S, p1, p2, p3, p4, E);
System.out.println(bellmanForm(S, points)); //輸出 null
}
// 這里改成了返回從`S`點到到其他所有點的最短路徑
public static Map<Point, Integer> bellmanForm(Point S, List<Point> points) {
Map<Point, Integer> L1 = new HashMap<>(); // L1列表,為了加快效率這里使用HashMap, key=Point value=length
for (Point p : points) L1.put(p, p == S ? 0 : Integer.MAX_VALUE / 2);
// 松弛操作
for (int t = 1; t < points.size(); t++) {
for (int i = 0; i < points.size(); i++) {
List<Point> children = points.get(i).children;
List<Integer> weight = points.get(i).weight;
for (int j = 0; j < children.size(); j++) {
if (L1.get(points.get(i)) > L1.get(children.get(j)) + weight.get(j)) {
L1.put(points.get(i), L1.get(children.get(j)) + weight.get(j));
}
}
}
}
// 檢測負權回路
for (Point v : L1.keySet()) {
int distanceV = L1.get(v);
for (Point u : L1.keySet()) {
int distanceU = L1.get(u);
int w = Integer.MAX_VALUE / 2;
if (u.children.contains(v)) w = u.weight.get(u.children.indexOf(v));
if (distanceV > distanceU + w) return null;
}
}
return L1;
}
// 定義節點類型
private static class Point {
char name;
List<Point> children = new ArrayList<>(); // 子節點
List<Integer> weight = new ArrayList<>(); // 邊的權重
Point(char n) {
this.name = n;
}
// 插入的時候注意,權重最小的放在最前面
void addChild(Point p, int w) {
int insertIndex = Collections.binarySearch(weight, w, (a, b) -> a - b); // 二分法查找插入index
if (insertIndex < 0) insertIndex = -(insertIndex + 1);
children.add(insertIndex, p);
weight.add(insertIndex, w);
}
}
}
如果我們不定義Point而是定義Edge數據結構,則松弛操作的寫法會更簡單,請讀者自行實現。
關于為什么松弛操作要循環V-1
次,這里簡單說明下:每次松弛都只松弛一層,如果結果是可求的話,那么路徑長度最多為V-1
,在最壞的情況下,對于最前面的邊的松弛操作需要V-1
次才能傳遞到路徑上的最后一條邊(入下圖所示),所以這里循環V-1
次。
Bellman-Form算法的時間復雜度為O(V*E),主要是在松弛的時候需要對
V
和E
做雙層的循環。
以上。