最短路径是指两个顶点之间权值之和最小的路径,有向图,无向图均适用,不能有负权环.
最短路径-无权图
无权图相当于是全部边的权值为1.
最短路径-负权边
有负权边,但是没有负权环时,存在最短路径.
最短路径-负权环
有负权环时,不存在最短路径
通过负权环,A到E的路径可以无限短
A->E->D->F->E->D->F->E......
求解最短路径3个经典算法
单源最短路径:
- Dijkstra
- Bellman-Ford
多源最短路径:
- Floyd
Dijkstra
属于单源最短路径算法,用于计算一个顶点到其他所有顶点的最短路径. 使用前提:不能有负权边
等价思考
Dijkstra的原理其实跟生活中的一些自然现象完全一样.
- 把每个顶点想象成一个小石头
- 每一条边想象成是一条绳子,每一条绳子都连着2块石头,边的权值就是绳子的长度
- 将小石头和绳子平放在一张桌子上
- 接下来想象一下,手拽着小石头A,慢慢地向上提起来,远离桌面
- B,D,C,E会依次离开桌面
- 最后绷直的绳子就是A到其他小石头的最短距离
红色的线会被拉直.
执行过程
松弛操作:更新2个顶点之间的最短距离,这里一般是指,更新源点到另一个点的最短距离,尝试找出更短的最短路径.
上图中,确定A到D的最短路径之后,对DC,DE边进行松弛操作,更新A到C,A到E的最短距离.
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<>();
//所有的path信息
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()) {
//获取paths里权值最小的边
Map.Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = getMinPath(paths);
//离开桌面的顶点
Vertex<V, E> minVertex = minEntry.getKey();
PathInfo<V, E> minPath = minEntry.getValue();
selectedPaths.put(minVertex.value, minPath);
//从paths里移除
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);
}
}
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());
}
private Map.Entry<Vertex<V, E>, PathInfo<V, E>> getMinPath(Map<Vertex<V, E>, PathInfo<V, E>> paths) {
Iterator<Map.Entry<Vertex<V, E>, PathInfo<V, E>>> iterator = paths.entrySet().iterator();
Map.Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = iterator.next();
while (iterator.hasNext()) {
Map.Entry<Vertex<V, E>, PathInfo<V, E>> entry = iterator.next();
if (weightManager.compare(entry.getValue().weight, minEntry.getValue().weight) < 0) {
minEntry = entry;
}
}
return minEntry;
}
Bellman-Ford
也属于单源最短路径算法,支持负权边,还能检测出是否有负权环.
对所有的边进行V-1次松弛操作(V是节点数量),得到所有可能的最短路径.
最好情况下对所有边仅需1次松弛操作就能计算出A到其他所有顶点的最短路径,如图:
最坏情况是恰好每次都从右到左的顺序对边进行松弛操作,对所有的边需进行V-1次松弛操作才能计算出A到其他所有顶点的最短距离.如图:
正常情况示例,一共8条边,每次松弛操作的边为:DC,DF,BC,ED,EF,BE,AE,AB,经过4次松弛操作之后,已经计算出A到其他所有顶点的最短路径,如图:
代码:
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;
for (int i = 0; i < count; i++) { // v - 1 次
for (Edge<V, E> edge : edges) {
PathInfo<V, E> fromPath = selectedPaths.get(edge.from.value);
if (fromPath == null) continue;
relax(edge, fromPath, selectedPaths);
}
}
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;
}