目录
最短路径(Shortest Path)
最短路径是指两顶点之间权值之和最小的路径(有向图、无向图均适用,不能有负权环)
最短路径 – 无权图
无权图相当于是全部边权值为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的原理其实跟生活中的一些自然现象完全一样
1. 把每1个顶点想象成是1块小石头, 每1条边想象成是1条绳子, 每一条绳子都连接着2块小石头, 边的权值就是绳子的长度; 将小石头和绳子平放在一张桌子上(下图是一张俯视图,图中黄颜色的是桌子)
2. 接下来想象一下, 手拽着小石头A, 慢慢地向上提起来,远离桌面
B、D、C、E会依次离开桌面, 最后绷直的绳子就是A到其他小石头的最短路径
Dijkstra – 执行过程
0. 初始化
1. 找到从A出发权重最小的边, 该边的to顶点将被第一个提起来, 那么A到B的最短路径将被确认; 对已确认最短路劲的B顶点的所有出边进行松弛操作, 尝试更新B顶点所有出边的to顶点的最短路径
2. 在第1步中尝试更新了所有顶点的最短路径, 那么此时再次从所有路径中查询出最小的路径(A-D), 该路径的to顶点将被提起, 即to顶点的最短路径将被确认; 然后, 对已确认最短路劲的D顶点的所有出边进行松弛操作, 尝试更新D顶点所有出边的to顶点的最短路径
松弛操作 (Relaxation): 更新2个顶点之间的最短路径
这里一般是指: 更新源点到另一个点的最短路径
松弛操作的意义: 尝试找出更短的最短路径
确定A到D的最短路径后, 对DC、DE边进行松弛操作, 更新了A到C、A到E的最短路径
3. 在第 2 步中尝试更新了所有顶点的最短路径, 那么此时再次从所有路径中查询出最小的路径(A-D-C), 该路径的to顶点将被提起, 即to顶点的最短路径将被确认; 然后, 对已确认最短路劲的C顶点的所有出边进行松弛操作, 尝试更新C顶点所有出边的to顶点的最短路径
4. 在第 3 步中尝试更新了所有顶点的最短路径, 那么此时再次从所有路径中查询出最小的路径(A-D-C-E), 该路径的to顶点将被提起, 即to顶点的最短路径将被确认; 然后, 对已确认最短路劲的E顶点的所有出边进行松弛操作, 尝试更新E顶点所有出边的to顶点的最短路径, 至此, 所有顶点的最短路径均已确认, 原始路径集合为空, 退出循环
Dijkstra – 代码实现
@Override
public 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();
//将起始顶点提前放入selectedPaths中, 避免对fromVertex顶点进行松弛操作,在后面需要再次移除
selectedPaths.put(begin,null);
//初始化起始顶点所有出边的权重
Iterator<Edge<V, E>> it = beginVertex.outEdges.iterator();
while(it.hasNext()){
Edge<V, E> edge = it.next();
PathInfo<V, E> pathInfo = new PathInfo<>();
pathInfo.weight = edge.weight;
pathInfo.edgeInfos.add(edge.info());
paths.put(edge.to,pathInfo);
}
while(!paths.isEmpty()) {
//将最小权重的顶点放入selectedPaths中
Map.Entry<Vertex<V, E>, PathInfo<V,E>> minPath = findMinPath(paths);
selectedPaths.put(minPath.getKey().value, minPath.getValue());
paths.remove(minPath.getKey());
Vertex<V, E> minVertex = minPath.getKey();
Iterator<Edge<V, E>> outEdges = minVertex.outEdges.iterator();
//对minVertex顶点的所有出边的to顶点进行松弛操作
while (outEdges.hasNext()) {
Edge<V, E> edge = outEdges.next();
if (selectedPaths.containsKey(edge.to.value)) continue;
relaxForDijkstra(edge,selectedPaths.get(edge.from.value),paths);
}
}
//移除起始顶点
selectedPaths.remove(begin);
return selectedPaths;
}
/**
* 松弛操作
* @param edge 进行松弛操作的边
* @param fromPath 起始顶点到edge的from顶点的最短路径
* @param paths 顶点与路径(非最短路径)的映射
*/
private void relaxForDijkstra(Edge<V, E> edge,PathInfo<V,E> fromPath, Map<Vertex<V,E>,PathInfo<V,E>> paths){
//begin顶点到to顶点的路径
PathInfo<V,E> oldPath = paths.get(edge.to);
//begin顶点到from顶点的权重 + 获取from顶点到to顶点的权重
E newWeight = weightManager.add(fromPath.weight, edge.weight);
//比较权重, 进行松弛操作
if (oldPath == null || weightManager.compare(newWeight,oldPath.weight) < 0) {
if(oldPath == null){
oldPath = new PathInfo<>();
paths.put(edge.to, oldPath);
}
/**
* 将[begin--from]路径与[from--to]路径进行拼接, [begin--from]路径为begin顶点到from顶点最短路径
* 将[begin--from]路径在paths中更新, 后面通过获取最小路径后,再放入selectedPaths
*/
oldPath.weight = newWeight;
List<EdgeInfo<V, E>> edgeInfos = oldPath.edgeInfos;
edgeInfos.clear();
edgeInfos.addAll(fromPath.edgeInfos); //将[begin--from]路径集合加入到edgeInfos集合中
edgeInfos.add(edge.info()); //将[from--to]路径加入到edgeInfos集合中
}
}
Bellman-Ford (贝尔曼-福特算法)
Bellman-Ford – 思路
Bellman-Ford 也属于单源最短路径算法,支持负权边,还能检测出是否有负权环
算法原理: 对所有的边进行 V – 1 次松弛操作(V是节点数量), 得到所有可能的最短路径
时间复杂度: O(EV), E是边数量, V是节点数量
最好情况是恰好从左到右的顺序对边进行松弛操作
对所有边仅需进行1次松弛操作就能计算出A到达其他所有顶点的最短路径
最坏情况是恰好每次都从右到左的顺序对边进行松弛操作
对所有边需进行 V – 1 次松弛操作才能计算出A到达其他所有顶点的最短路径
Bellman-Ford – 思考
1. 如何判断图中是否存在负权环?
在进行 V-1 次松弛操作后, 再进行一轮松弛操作, 在这轮松弛操作中某个顶点再次计算出了更短的路径, 那么说明图中存在负权环
2. 为什么最多需要 V-1 次松弛操作就可以确认所有顶点的最短路径?
最坏情况下, 某一个顶点E的最短路径需要经过所有的顶点也就是顶点E的最短路径会存在V-1条边, 那么顶点E最短路径确认的前提条件是其他顶点的最短路径全部确认; 那么在每轮松弛操作中, 至少会确认其中的一条边的为顶点E最短路径中的一个边, 那么总共需要V-1轮松弛操作 (每一个轮的松弛操作都是在上一轮松弛操作更新路径的基础上)
3. 某一个顶点E的最短路径需要经过所有的顶点为什么会是最坏情况?
如果一个顶点顶点E的最短路径并不需要经过所有的顶点, 那么一定存在以下情况: 至少存在两个顶点依赖一个顶点的最短路径, 也就是在一轮松弛操作后, 某一个顶点确认之后, 下一轮松弛操作将至少确认两个顶点的最短路径; 如下, 当顶点B的路径更新后, 下一轮松弛操作中顶点E和顶点F的路径可以同时更新, 将少一轮松弛操作, 也就是低于V-1轮松弛操作就可以确认所有顶点的最短路径
以上为个人理解, 如果有不对的地方, 欢迎指出
Bellman-Ford – 执行过程
不难分析出,经过4次松弛操作之后,已经计算出了A到其他所有顶点的最短路径, 在第五次松弛操作时没有更新任何一个顶点的最短路径
Bellman-Ford – 代码实现
@Override
public Map<V, PathInfo<V, E>> bellman_ford(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin);
if(beginVertex == null) return null;
Map<V,PathInfo<V,E>> paths = new HashMap();
PathInfo<V, E> beginPathInfo = new PathInfo<>();
beginPathInfo.weight = weightManager.zero();
paths.put(begin,beginPathInfo);
int count = vertices.size() - 1;
for (int i = 0; i < count - 1; i++) {
boolean relaxSucc = false;
for (Edge<V, E> edge : edges) {
if (relaxSucc){
relaxForBellman_Ford(edge,paths.get(edge.from.value),paths);
}else{
relaxSucc = relaxForBellman_Ford(edge,paths.get(edge.from.value),paths);
}
}
/**
* 如果在 V-1次以内的某一轮循环中, 没有执行有效的松弛操作, 那么说明, 所有顶点的最短路径均已确认, 直接退出;
*/
if (!relaxSucc) break;
}
/**
* 负权环检测
*/
for (Edge<V, E> edge : edges) {
if (relaxForBellman_Ford(edge,paths.get(edge.from.value),paths)){
System.out.println("存在负权环!");
paths.clear();
return paths;
}
}
/**
* 如果在V-1次以内的某一轮循环中, 没有执行有效的松弛操作, 那么说明, 所有顶点的最短路径均已确认, 直接退出;
*/
paths.remove(begin);
return paths;
}
/**
* 松弛操作
* @param edge 进行松弛操作的边
* @param fromPath 起始顶点到edge的from顶点的路径
* @param paths 顶点与路径(非最短路径)的映射
* @return 是否进行了松弛操作
*/
private boolean relaxForBellman_Ford(Edge<V, E> edge,PathInfo<V,E> fromPath, Map<V,PathInfo<V,E>> paths){
//begin顶点到to顶点的路径
PathInfo<V,E> oldPath = paths.get(edge.to.value);
//begin顶点到from顶点的权重 + 获取from顶点到to顶点的权重
E newWeight = weightManager.add(fromPath.weight, edge.weight);
//比较权重, 进行松弛操作
if (oldPath == null || weightManager.compare(newWeight,oldPath.weight) < 0) {
if(oldPath == null){
oldPath = new PathInfo<>();
paths.put(edge.to.value, oldPath);
}
/**
* 将[begin--from]路径与[from--to]路径进行拼接, [begin--from]路径为begin顶点到from顶点最短路径
* 将[begin--from]路径在paths中更新, 后面通过获取最小路径后,再放入selectedPaths
*/
oldPath.weight = newWeight;
List<EdgeInfo<V, E>> edgeInfos = oldPath.edgeInfos;
edgeInfos.clear();
edgeInfos.addAll(fromPath.edgeInfos); //将[begin--from]路径集合加入到edgeInfos集合中
edgeInfos.add(edge.info()); //将[from--to]路径加入到edgeInfos集合中
return true;
}else{
return false;
}
}
Floyd (弗洛伊德算法)
Floyd 属于多源最短路径算法, 能够求出任意2个顶点之间的最短路径, 支持负权边
时间复杂度:O(V3), 效率比执行 V 次 Dijkstra 算法要好( V 是顶点数量)
Floyd – 算法原理
从任意顶点 i 到任意顶点 j 的最短路径不外乎两种可能
1.直接从 i 到 j
2.从i经过若干个顶点到 j
(1) 假设 dist(i, j) 为顶点 i 到顶点 j 的最短路径的距离
(2) 对于每一个顶点 k, 检查 dist(i, k) + dist(k, j) < dist(i,j) 是否成立
a. 如果成立, 证明从 i 到 k 再到 j 的路径比 i 直接到 j 的路径短, 设置 dist(i, j) = dist(i, k) + dist(k, j)
b. 当我们遍历完所有结点 k, dist(i, j) 中记录的便是 i 到 j 的最短路径的距离
Floyd – 代码实现
@Override
public Map<V, Map<V, PathInfo<V, E>>> floyd() {
Map<V, Map<V, PathInfo<V, E>>> paths = new HashMap<>();
//初始化paths, 确保每个顶点都在paths中作为from顶点, 即paths.get(v) != null
edges.forEach((Edge<V, E> edge) -> {
Map<V, PathInfo<V, E>> fromPathMap = paths.get(edge.from.value);
if(fromPathMap == null){
fromPathMap = new HashMap();
paths.put(edge.from.value,fromPathMap);
}
PathInfo<V, E> toPathInfo = new PathInfo<>();
toPathInfo.edgeInfos.add(edge.info());
toPathInfo.weight = edge.weight;
fromPathMap.put(edge.to.value,toPathInfo);
});
vertices.forEach((V v2, Vertex<V, E> vertiex2) -> { //中间节点
vertices.forEach((V v1, Vertex<V, E> vertiex1) -> { //from节点
vertices.forEach((V v3, Vertex<V, E> vertiex3) -> { //to节点
if (v1.equals(v2) || v2.equals(v3) || v1.equals(v3)) return;
// 获取v1到v2路径的权重
PathInfo<V, E> path12 = getPathInfo(v1,v2,paths);
if (path12 == null) return ;
// 获取v2到v3路径的权重
PathInfo<V, E> path23 = getPathInfo(v2,v3,paths);
if(path23 == null) return ;
// 获取v1到v3原路径的权重
PathInfo<V, E> path13 = getPathInfo(v1,v3,paths);
E newWeight = weightManager.add(path12.weight,path23.weight);
//比较[v1--v2]权重 + [v2--v3]权重 与 [v1--v3]权重的大小, 判断是否需要更新v1到v2的路径
if(path13 == null || weightManager.compare(newWeight, path13.weight) < 0){
if(path13 == null){ //如果原v1->v3路径为空,则初始化, 并加入到v1的路径信息中
path13 = new PathInfo<>();
paths.get(v1).put(v3,path13);
}else{
path13.edgeInfos.clear();
}
path13.weight = newWeight;
path13.edgeInfos.addAll(path12.edgeInfos);
path13.edgeInfos.addAll(path23.edgeInfos);
}
});
});
});
return paths;
}
/**
* 获取指定from顶点和to顶点的路径信息
* @param from
* @param to
* @param paths
* @return
*/
private PathInfo<V, E> getPathInfo(V from,V to,Map<V, Map<V, PathInfo<V, E>>> paths){
Map<V, PathInfo<V, E>> pathInfo = paths.get(from);
return pathInfo == null ? null : pathInfo.get(to);
}