所谓最短路径问题是指:如果从图中某一顶点(源点)到达另一顶点(终点)的路径可能不止一条,如何找到一条路径使得沿此路径上各边的权值总和(称为路径长度)达到最小。
前提:不能有负权边!
维护一张哈希表,表中存储从起点到其他顶点的最短路径信息(经过的结点和最短路径)。将哈希表初始化即起点到起点的最短路径置为0,从表中选出最短的那条路径(边的终点不能在结果集中)加入结果集并提起对应的顶点,之后对邻边进行松弛,还需要将刚刚提起的结点从哈希表中删除。如此循环反复,直到结果集中包括了起点到其他顶点的所有最短路径信息或者哈希表为空,算法结束。
松弛:判断是否要更新由起点到提起顶点出度边终点的最短路径信息。
/**
* 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;
}
前提:无负权环,可有负权边。
维护一张哈希表,表中存储从起点到其他顶点的最短路径信息(经过的结点和最短路径)。将哈希表初始化即起点到起点的最短路径置为0,之后对所有边进行 n-1 次松弛
,n是顶点个数。
BellmanFord算法完成之后,再对所有边松弛一次,如果还能松弛成功的话,代表图里有负权环。
/**
* 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;
}
算法伪代码如下:
每选择一次u这个点,里面两层for循环会判断原先v和w之间的最短路径是否比经过u中转的路径要长,要长的话就更新v和w之间的最短路径为v->u->w。整个图里面每个点都成为u点之后,算法结束。
必须搞清楚 Java 里面对象在堆空间的内存地址分配!
/**
* 多源最短路径
*
* @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;
}