一、最短路徑簡介
所謂最短路徑問題是指:如果從圖中某一頂點(源點)到達另一頂點(終點)的路徑可能不止一條,如何找到一條路徑使得沿此路徑上各邊的權值總和(稱爲路徑長度)達到最小。
路徑規劃就是最短路徑的典型應用。
二、Dijkstra算法(單源最短路徑)
1、算法思路
前提:不能有負權邊!
維護一張哈希表,表中存儲從起點到其他頂點的最短路徑信息(經過的結點和最短路徑)。將哈希表初始化即起點到起點的最短路徑置爲0,從表中選出最短的那條路徑(邊的終點不能在結果集中)加入結果集並提起對應的頂點,之後對鄰邊進行鬆弛,還需要將剛剛提起的結點從哈希表中刪除。如此循環反覆,直到結果集中包括了起點到其他頂點的所有最短路徑信息或者哈希表爲空,算法結束。
鬆弛:判斷是否要更新由起點到提起頂點出度邊終點的最短路徑信息。
2、實現
/**
* Dijkstra算法, 只適用無負權邊的情況
*
* @param element 起點
* @return 起點到其他所有頂點的最短路徑信息
*/
private Map<E, PathInfo<E, V>> dijkstra(E element) {
Vertex<E, V> beginVertex = vertices.get(element);
if (beginVertex == null)
return null;
Map<E, PathInfo<E, V>> result = new HashMap<>();
Map<Vertex<E, V>, PathInfo<E, V>> paths = new HashMap<>();
PathInfo<E, V> beginVertexInfo = new PathInfo<>();
beginVertexInfo.shortest = (V) weightAbout.zero();
paths.put(beginVertex, beginVertexInfo);
while (!paths.isEmpty()) {
Map.Entry<Vertex<E, V>, PathInfo<E, V>> minPathInfos = getMinPathInfos(paths); //獲取最短路徑信息
Vertex<E, V> startVertex = minPathInfos.getKey();
for (Edge<E, V> edge : startVertex.outEdges) { //鬆弛相鄰邊
//松馳過的邊以及指向起點的邊不進行松馳操作
if (result.containsKey(edge.to.element) || edge.to.element.equals(element))
continue;
//鬆弛操作(提起的最小邊的信息, 需要鬆弛的邊, 結點的路徑信息)
relax(minPathInfos.getValue(), edge, paths);
}
result.put(minPathInfos.getKey().element, minPathInfos.getValue()); //將選出的最短路徑信息加入結果集
paths.remove(minPathInfos.getKey()); //從哈希表中刪除已經確定最短路徑信息的結點
}
result.remove(element); //刪除起點到起點
// 的最短路徑信息
return result;
}
/**
* 鬆弛操作
*
* @param fromPathInfo 提起的最小邊的信息
* @param edge 需要鬆弛的邊
* @param paths 結點的路徑信息
*/
private boolean relax(PathInfo<E, V> fromPathInfo, Edge<E, V> edge, Map<Vertex<E, V>, PathInfo<E, V>> paths) {
PathInfo<E, V> nextPathInfos = paths.get(edge.to);
V newWeight = (V) weightAbout.add(fromPathInfo.shortest, edge.weight);
V oldWeight = nextPathInfos == null ? (V) weightAbout.zero() : nextPathInfos.shortest;
if (nextPathInfos != null && weightAbout.compareWeight(newWeight, oldWeight) >= 0)
return false; //新權重比老權重大則鬆弛失敗
if (nextPathInfos == null || weightAbout.compareWeight(newWeight, oldWeight) < 0) {
//老路徑不存在或者新路徑權值較小則鬆弛成功
if (nextPathInfos == null) {
nextPathInfos = new PathInfo<>();
paths.put(edge.to, nextPathInfos);
} else {
nextPathInfos.edges.clear(); //老路徑不爲空就清空以前的路徑信息
}
nextPathInfos.shortest = newWeight;
nextPathInfos.edges.addAll(fromPathInfo.edges);
nextPathInfos.edges.add(edge.castToInfo());
}
return true;
}
/**
* 從哈希表中獲取最短路徑的信息
*
* @param paths 存儲着起點到其他所有結點最短路徑信息的哈希表
* @return 最短路徑信息
*/
private Map.Entry<Vertex<E, V>, PathInfo<E, V>> getMinPathInfos(Map<Vertex<E, V>, PathInfo<E, V>> paths) {
Iterator<Map.Entry<Vertex<E, V>, PathInfo<E, V>>> it = paths.entrySet().iterator();
Map.Entry<Vertex<E, V>, PathInfo<E, V>> minEntry = it.next();
while (it.hasNext()) {
Map.Entry<Vertex<E, V>, PathInfo<E, V>> entry = it.next();
if (weightAbout.compareWeight(minEntry.getValue().shortest, entry.getValue().shortest) > 0)
minEntry = entry;
}
return minEntry;
}
三、BellmanFord算法(單源最短路徑)
1、算法思路
前提:無負權環,可有負權邊。
維護一張哈希表,表中存儲從起點到其他頂點的最短路徑信息(經過的結點和最短路徑)。將哈希表初始化即起點到起點的最短路徑置爲0,之後對所有邊進行 n-1 次鬆弛
,n是頂點個數。
BellmanFord算法完成之後,再對所有邊鬆弛一次,如果還能鬆弛成功的話,代表圖裏有負權環。
2、實現
/**
* BellmanFord算法, 支持負權邊, 而且還能檢測負權環是否存在
*
* @param element 起點
* @return 起點到其他所有頂點的最短路徑信息
*/
private Map<E, PathInfo<E, V>> bellmanFord(E element) {
Vertex<E, V> beginVertex = vertices.get(element);
if (beginVertex == null)
return null;
Map<E, PathInfo<E, V>> result = new HashMap<>();
Map<Vertex<E, V>, PathInfo<E, V>> paths = new HashMap<>();
//初始化起點的權值爲zero, 保證第一次鬆弛至少有一個結點鬆弛成功
PathInfo<E, V> beginPathInfo = new PathInfo<>();
beginPathInfo.shortest = (V) weightAbout.zero(); //暫時將起點的最短路徑加入到結果集
paths.put(beginVertex, beginPathInfo);
final int COUNT = vertices.size() - 1;
for (int index = 0; index < COUNT; index++) {
for (Edge<E, V> edge : edges) {
PathInfo<E, V> fromPathInfo = paths.get(edge.from);
if (fromPathInfo == null) //到起點沒有路徑信息就跳過
continue;
relax(fromPathInfo, edge, paths);
}
}
for (Edge<E, V> edge : edges) {
if (relax(paths.get(edge.from), edge, paths)) {
System.out.println("圖裏有負權環!");
return null;
}
}
Set<Vertex<E, V>> verticesSet = paths.keySet(); //格式轉換而已
for (Vertex<E, V> vertex : verticesSet) {
result.put(vertex.element, paths.get(vertex));
}
result.remove(element); //刪除起點到起點的最短路徑
return result;
}
四、Floyd算法(多源最短路徑)
1、算法思路
算法僞代碼如下:
每選擇一次u這個點,裏面兩層for循環會判斷原先v和w之間的最短路徑是否比經過u中轉的路徑要長,要長的話就更新v和w之間的最短路徑爲v->u->w。整個圖裏面每個點都成爲u點之後,算法結束。
2、注意點
必須搞清楚 Java 裏面對象在堆空間的內存地址分配!
3、實現
/**
* 多源最短路徑
*
* @return 每個點到其他結點的最短路徑信息
*/
private Map<E, Map<E, PathInfo<E, V>>> floyd() {
Map<E, Map<E, PathInfo<E, V>>> result = new HashMap<>();
for (Edge<E, V> edge : edges) { //初始化
Map<E, PathInfo<E, V>> value = result.get(edge.from.element);
if (value == null) {
value = new HashMap<>();
result.put(edge.from.element, value);
}
PathInfo<E, V> pathInfo = value.get(edge.to.element);
if (pathInfo == null) {
pathInfo = new PathInfo<>();
value.put(edge.to.element, pathInfo);
}
pathInfo.shortest = edge.weight;
pathInfo.edges.add(edge.castToInfo());
}
vertices.forEach((E e1, Vertex<E, V> vertex1) -> {
vertices.forEach((E e2, Vertex<E, V> vertex2) -> {
vertices.forEach((E e3, Vertex<E, V> vertex3) -> {
if (e1.equals(e2) || e2.equals(e3) || e3.equals(e1))
return; //相等直接返回, forEach的return相當於continue
PathInfo<E, V> path21 = result.get(e2) == null ? null : result.get(e2).get(e1);
if (path21 == null)
return; //閉包的return等價於continue
PathInfo<E, V> path13 = result.get(e1) == null ? null : result.get(e1).get(e3);
if (path13 == null)
return;
PathInfo<E, V> path23 = result.get(e2) == null ? null : result.get(e2).get(e3);
V newWeight = (V) weightAbout.add(path21.shortest, path13.shortest);
V oldWeight = path23 == null ? (V) weightAbout.zero() : path23.shortest;
if (path23 != null && weightAbout.compareWeight(newWeight, oldWeight) >= 0)
return;
if (path23 == null) {
path23 = new PathInfo<>();
result.get(e2).put(e3, path23);
} else {
path23.edges.clear();
}
path23.shortest = newWeight;
path23.edges.addAll(path21.edges);
path23.edges.addAll(path13.edges);
});
});
});
return result;
}