961全部内容链接
带权路径长度:带权图中,一个节点u到另一个节点v所经过的边的权值之和称为带权路径长度。一个带权图中,节点u到节点v有许多路径,其中权值之和最短的那一条称为最短路径。当然最短路径也可以针对无向图,比如利用BSF算法中求出节点的单源最短路径。
带权图的单源最短路径:就是求一个节点到所有其他节点的最短路径。Dijkstra算法就是干这个的。
带权图的每对顶点间的最短路径:使用Floyd算法解决。
Dijkstra(迪杰斯特拉)可以解决带权图单源最短路径问题。该算法可以同时解决两个问题:初始节点v到其他所有节点的最短路径是多少,并且还能解决到其他节点的最短路径是什么。
该算法既可以针对无向图也可以针对有向图。无向图就可以理解成两个方向都可以走。
具体思想为:
如图,假设问题是求v1到其他各个点的最短路径。 那么v1的邻接节点有v2和v4。其中
该算法需要初始三个集合:
v1 | v2 | v3 | v4 | v5 | v6 | v7 | |
---|---|---|---|---|---|---|---|
known | √ | × | × | × | × | × | × |
distance | 0 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ |
path | null | null | null | null | null | null | null |
其中known存储的是“节点的最短路径是否已经确定下来”。distance存储的是“当前已知的v1到该节点的最短路径是多少”。path存储的是“最短路径的上一个环节是哪个节点”。比如其中v1到v6的最短路径为 v1,v4,v3,v6,那么path[v6]=v3,path[v3]=v4,path[v4]=v1,path[v1]=null。
整个算法语言描述如下(这里使用v1代指起始节点):
Java代码如下:
public static Map dijkstra(WeightedGraph graph, Object vertex) {
Set known = new HashSet(); // 存储都有哪些节点的最短路径已经确定下来了
Map<Object, Integer> distance = new HashMap<>(); // 存储vertex节点到其他节点的最短路径的路径长度
Map<Object, Object> path = new HashMap<>(); // 存储vertex到其他节点的路径。Key为节点,Value为该节点最短路径的上一个环节是哪一个节点
for (Object itemVertex : graph.getVertexes()) {
distance.put(itemVertex, Integer.MAX_VALUE); // 初始化vertex节点到其他的节点的距离,初始都为无穷
path.put(itemVertex, null); // 初始化vertex节点的路径,最开始都为null
}
distance.put(vertex, 0); // 初始化vertex节点到自身的距离为0
// 初始化到此整个结束
while (known.size() < graph.getVertexNumber()) {
// 如果节点没有确定下来最短路径,则继续循环
Integer minDistance = Integer.MAX_VALUE;
Object minVertex = null;
// 找出distance中距离最小且还没有确定最短路径的节点
for (Object key : distance.keySet()) {
if (!known.contains(key) // 如果该节点还没有确定路径
&& distance.get(key) <= minDistance) // 并且比当前的最短路径要短
{
minDistance = distance.get(key); // 则更新当前最短路径和当前距离最小节点
minVertex = key;
}
}
known.add(minVertex); // 上面找出的距离最小的节点且
Object[] neighbors = graph.neighbors(minVertex); // 找出该节点的所有邻接节点
for (Object neighbor : neighbors) {
// 计算初始节点到该minVertex的最小路径+它邻居的路径。即求它的邻接节点从该节点绕过来所需用的最短路径
int tempDistance = graph.getEdgeWeight(minVertex, neighbor) + distance.get(minVertex);
if (tempDistance < distance.get(neighbor)) {
// 如果之前所求的初始节点到neighbor节点的距离比从当前节点绕道所走的路远,
// 则将neighbor的最短路径更新成从当前节点绕道走
distance.put(neighbor, tempDistance);
path.put(neighbor, minVertex);
}
}
}
return distance; // 如果返回path,则求得是最短路径是什么,返回distance求得是最短路径的长度
// return path;
}
复杂度分析:
适用性:dijkstra算法不适用于带负权值的图。比如:
对于该图,如果用dijkstra算法求得话,v0到v2的最短路径就是7,但实际上从v1绕道去v2可以更短,只需要10-5=5。
Floyd算法适用于求各个节点直接的最短路径长度和各个节点之间的最短路径。它的基本想法为:
算法实现需要借助一个二维数组。
数组arr | v1 | v2 | v3 | v4 | v5 | v6 | v7 |
---|---|---|---|---|---|---|---|
v1 | 0 | 2 | ∞ | 1 | ∞ | ∞ | ∞ |
v2 | ∞ | 0 | ∞ | 3 | 10 | ∞ | ∞ |
v3 | 4 | ∞ | 0 | ∞ | ∞ | 5 | ∞ |
v4 | ∞ | ∞ | 2 | 0 | 2 | 8 | 4 |
v5 | ∞ | ∞ | ∞ | ∞ | 0 | ∞ | 6 |
v6 | ∞ | ∞ | ∞ | ∞ | ∞ | 0 | ∞ |
v7 | ∞ | ∞ | ∞ | ∞ | ∞ | 1 | 0 |
该数组arr[i][j]表示 ,即节点i到节点j的当前最短距离。初始情况下,不允许绕路,所以初始数组如上表所示。
第一轮循环可以允许v1节点作为中间节点,即允许往v1节点绕路,所以即当
arr[i][j] > arr[i][v1] + arr[v1][j] 时,更新 arr[i][j] = arr[i][v1] + arr[v1][j]
这个上面提到过 arr[i][j] 就是当前已知最短路径,arr[i][v1]就是i节点到v1节点已知的最短路径,arr[v1][j]是v1节点到j节点的最短路径。如果从v1绕路更快,那么就把它换成从k走。
第二轮循环原理类似,直至所有的节点循环完毕。
同时,如果想要求出最短路径是什么,还可以再申请一个数组存储两个节点直接的中转节点。
Java代码如下:
public static long[][] floyd(WeightedGraph graph) {
Object[] vertexes = graph.getVertexes(); // 获取图中的所有节点
long[][] distances = new long[vertexes.length][vertexes.length]; // 申请数组保存各节点之间的距离
Object[][] path = new Object[vertexes.length][vertexes.length]; // 申请数组保存各个节点最短距离之间的中转节点
for (int i = 0; i < distances.length; i++) {
// 初始化数组
for (int j = 0; j < distances[i].length; j++) {
distances[i][j] = graph.getEdgeWeight(vertexes[i], vertexes[j]);
if (distances[i][j] == 0 && i!=j) {
distances[i][j] = Integer.MAX_VALUE;
}
}
}
// Floyd核心代码开始
for (int k = 0; k < vertexes.length; k++) {
// k代表中转节点,从v0开始一直到vn
for (int i = 0; i < vertexes.length; i++) {
// i代表起始节点,即路径中的i
for (int j = 0; j < vertexes.length; j++) {
// j代表目标节点,即路径中的j
if (distances[i][j] > distances[i][k] + distances[k][j]) {
// 如果通过k绕路比原先的快,则更新最短路径和中转节点
distances[i][j] = distances[i][k] + distances[k][j];
path[i][j] = vertexes[k];
}
}
}
}
return distances; // 返回各个最短路径长度
// return path; // 返回各个最短路径
}
Floyd算法稍微有些难理解,理解不了就背下来,反正没几行。时间复杂度容易看出是O(|V|^3)
适用性:
Floyd算法可以使用与带负权值的图。比如上述例子:
若采用Dijkstra算法肯定不行,这个上面说过了。但是如果用Floyd算法就可以。第一轮初始化arr[v0][v2] = 7。然后当允许通过v1中转时,arr[v0][v2]就会变成5。
但是Floyd不适用于带负权值且有回路的图。
比如这个图,这个图讲道理就没有最短路径,比如v0到v1的最短路径是-9,但是你要是再绕一圈,就变成了-11,再绕一圈就变成了-13。所以根本就不存在最短路径
BFS算法 | Dijkstra算法 | Floyd算法 | |
---|---|---|---|
无权图 | √ | √ | √ |
带权图 | × | √ | √ |
带负权值的图 | × | × | √ |
带负权值有回路的图 | × | × | × |
时间复杂度 | O(|V|^2) 或 O(|V|+|E|) | O(|V|^2) | O(|V|^3) |
通常用于 | 求无权图的单元最短路径 | 求带权图的单源最短路径 | 求带权图中各个顶点间的最短路径 |