最短路径
即找到从一个顶点到达另一个顶点的成本最小的路径。
单点最短路径。给定一幅加权有向图和一个起点s,回答“从s到给定的目的顶点v是否存在一条有向路径?如果有,找出最短(总权重最小)的那条路径。”
我们计划在本节讨论下列问题:
- 加权有向图的API和实现以及单点最短路径的API
- 解决边的权重非负的最短路径问题的经典Dijkstra算法;
- 在无环加权有向图中解决该问题的一种快速算法,边的权重甚至可以是负值
- 适用于一般情况的经典Bellman-Ford算法,其中图可以含有环,边的权重也可以是负值。
4.4.2 加权有向图的数据结构
+++
加权有向边的数据类型
/**
* 加权有向边的数据类型
*/
public class DirectedEdge {
private final int v; //边的起点
private final int w; //边的终点
private final double weight; //边的权重
public DirectedEdge(int v, int w, double weight) {
this.v = v;
this.w = w;
this.weight = weight;
}
public double weight() {
return weight;
}
public int from() {
return v;
}
public int to() {
return w;
}
@Override
public String toString() {
return String.format("%d->%d %.2f", v, w, weight);
}
}
DirectedEdge类的实现比4.3节无向边的数据类型Edge类更简单,因为边的两个端点是有区别的。用例可以使用惯用代码
int v=e.to(),w=e.from();来访问DirectedEdge的两个端点。
++++
加权有向图的数据类型
++++
/**
* 加权有向图
*/
public class EdgeWeightedDigraph {
private final int V; //顶点总数
private int E; //边总数
private Bag[] adj; //邻接表
public EdgeWeightedDigraph(int V) {
this.V = V;
this.E = 0;
adj = (Bag[]) new Bag[V];
for (int v = 0; v < V; v++) {
adj[v] = new Bag<>();
}
}
public int V() {
return V;
}
public int E() {
return E;
}
public void addEdge(DirectedEdge e) {
adj[e.from()].add(e);
E++;
}
public Iterable adj(int v) {
return adj[v];
}
public Iterable edges() {
Bag bag = new Bag<>();
for (int v = 0; v < V; v++) {
for (DirectedEdge e : adj[v])
bag.add(e);
}
return bag;
}
}
它维护了一个由顶点索引的Bag对象的数组,Bag对象的内容为DirectedEdge对象。与Digraph类一样,每条边的邻接表中只会出现一次:如果一条边从v指向w,那么它只会出现在v的邻接链表中。这个类可以处理自环和平行边。
下图是用EdgeWeightedDigraph表示左侧的加权有向图构造出的数据结构。
最短路径的API
4.4.2.3 最短路径的数据结构
- 最短路径树中的边。和深度优先搜索、广度优先搜索和Prim算法一样,使用一个由顶点索引的DirectedEdge对象的父链接数组edgeTo[],其中edgeTo[v]的值为树中连接v和它的父节点的边(也是从s到v的最短路径上的最后一条边)。
- 到达起点的距离。我们需要一个由顶点索引的数组distTo[],其中distTo[v]为从s到v的已知最短路径的长度。
我们约定,edgesTo[s]的值为null,distTo[s]的值为0,同时约定,从起点到不可达的顶点的距离均为Double.POSITIVE_INFINITY。
4.4.2.4 边的松弛
我们的最短路径API的实现基于一个被称为松弛的简单操作.一开始我们只知道图的边和它们的权重,distTo[]中只有起点所对应的元素值为0,其余元素的值均被初始化为Double.POSITIVE_INFINITY。随着算法的执行,它将起点到其他顶点的最短路径信息存入了edgeTo[]和distTo[]数组中。在遇到新的边的时候,通过更新这些信息就可以得到新的最短路径。
我们在其中会用到边的松弛技术,定义如下:放松边v->w意味着检查从s到w的最短路径是否先从s到v,然后再由v到w。如果是,则根据这个情况更新数据结构的内容。下面代码实现了这个操作。由v到w的最短路径是distTo[v]与e.weight()之和——如果这个值不小于distTo[w],则称这条边失效了并忽略;如果这个值更小,则更新数据。
4.4.2.5 顶点的松弛
实际上,实现会放松从一个给定顶点只会的所有边,如下面(被重载的)relax()的实现所示。注意,从任意distTo[v]为有限值的顶点v指向任意distTo[]为无穷的顶点都是有效的。如果v被放松,那么这些有效边就会被添加到edgeTo[]中。某条从起点指出的边将会是第一条被加入edgeTo[]中的边。算法会谨慎选择顶点,使得每次顶点松弛操作都能得出到达某个顶点的最短的路径,最后逐渐找出到达每个顶点的最短路径。
4.4.2.6 为用例准备的查询方法
edgeTo[]和distTo[]直接支持pathTo()、hasPathTo()和distTo()的查询方法。默认所有最短路径的实现都包含这段代码。前面已经提到过,只有v是从s可达的情况下,distTo[v]才是有意义的,还约定,对于从s不可达的顶点,distTo()方法都应该返回无穷大。在实现这个约定时,将distTo[]中所有元素都初始化为Double.POSITIVE_INFINITY,distTo[s]则为0。最短路径算法会将从起点可达的顶点v的distTo[v]设为一个有限制,这样就不必再用marked[]数组来在图的搜索中标记可达的顶点,而是检查distTo[v]是否为Double.POSITIVE_INFINITY来实现hasPathTo(v)。对于pathTo()方法,我们约定如果v不是从起点科大的则返回null,如果v等于起点返回一条不含任何边的路径
4.4.3最短路径算法的理论基础
4.4.3.1 最优性条件
一下命题证明了判断路径是否最短路径的全局条件与放松一条边的时候检测的局部条件是否等价。
4.4.3.3 通用算法
由最优性条件可以得到一个涵盖所有最短路径的通用算法。现在,我们暂时只研究非负权重的情况。
通用算法没有指定边的放松顺序。
Dijkstra算法
Dijkstra首先将distTo[s]初始化为0,distTo[]中的其他元素初始化为正无穷。然后将distTo[]最小的非树顶点放松并加入树中,知道所有的顶点都在树中或所有的非树顶点的distTo[]值为无穷大
4.4.4.1 数据结构
要实现Dijkstra算法,除了distTo[]和edgeTo[]外,还需要一条索引优先队列pq,保存需要放松的顶点并确认下一个被放松的顶点。
代码实现:
public class DijkstraSP {
private DirectedEdge[] edgeTo;
private double[] distTo;
private IndexMinPQ pq;
public DijkstraSP(EdgeWeightedDigraph G, int s) {
edgeTo = new DirectedEdge[G.V()];
distTo = new double[G.V()];
pq = new IndexMinPQ<>(G.V());
for (int i = 0; i < G.V(); i++) {
distTo[i] = Double.POSITIVE_INFINITY;
}
distTo[s] = 0.0;
pq.insert(s, 0.0);
while (!pq.isEmpty())
relax(G, pq.delMin());
}
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
if (pq.contains(w)) pq.change(w, distTo[w]);
else pq.insert(w, distTo[w]);
}
}
}
public double distTo(int v) {
return distTo[v];
}
public boolean hasPathTo(int v) {
return distTo[v] < Double.POSITIVE_INFINITY;
}
public Iterable pathTo(int v) {
if (!hasPathTo(v)) return null;
Stack path = new Stack<>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
path.push(e);
}
return path;
}
}
4.4.5 无环加权有向图的最短路径算法
许多应用中的加权有向图都是不含有有向环的,我们现在来学习一种比Dijkstra算法更快、更简单的在无环加权有向图中找出最短路径的算法,它的特点是
- 能够在线性时间内解决单点最短路径问题
- 能够处理负权重的边
- 能够解决相关的问题,例如找出最长路径。
这些算法都是无环有向图的拓扑排序算法的简单扩展。
特别的是,只要将顶点的放松和拓扑排序结合起来,马上就能得到一种解决无环加权有向图中最短路径问题的算法。
首先,将distTo[s]初始化为0,其他distTo[]元素初始化为无穷大,然后一个个按照拓扑排序顺序放松所有顶点
算法 无环加权有向图的最短路径算法
/**
* 无环加权有向图的最短路径算法
*/
public class AcyclicSp {
private DirectedEdge[] edgeTo;
private double[] distTo;
public AcyclicSp(EdgeWeightedDigraph G, int s) {
edgeTo = new DirectedEdge[G.V()];
distTo = new double[G.V()];
for (int v = 0; v < G.V(); v++) {
distTo[v] = Double.POSITIVE_INFINITY;
}
distTo[s] = 0.0;
Topological top = new Topological(G);
for (int v : top.order()) {
relax(G, v);
}
}
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
}
}
}
public double distTo(int v) {
return distTo[v];
}
public boolean hasPathTo(int v) {
return distTo[v] < Double.POSITIVE_INFINITY;
}
public Iterable pathTo(int v) {
if (!hasPathTo(v)) return null;
Stack stack = new Stack<>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
stack.push(e);
}
return stack;
}
}
4.4.5.1 最长路径
无环加权有向图中的单点最长路径。给定一幅无环加权有向图(边的权重可为负)和一个起点s,回答“是否存在一条从s到给定的顶点v的路径?如果有,找出最长的那条路径”
实现该类的一个简单方法是修改AcyclicSP,将distTo[]的初始值变为Double.NEGATIVE_INFINITY并改变relax()不等式的方向
/**
* 无环加权有向图的最长路径算法
*/
public class AcyclicLP {
private DirectedEdge[] edgeTo;
private double[] distTo;
public AcyclicLP(EdgeWeightedDigraph G, int s) {
edgeTo = new DirectedEdge[G.V()];
distTo = new double[G.V()];
for (int v = 0; v < G.V(); v++) {
distTo[v] = Double.NEGATIVE_INFINITY;
}
distTo[s] = 0.0;
Topological top = new Topological(G);
for (int v : top.order()) {
relax(G, v);
}
}
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] < distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
}
}
}
public double distTo(int v) {
return distTo[v];
}
public boolean hasPathTo(int v) {
return distTo[v] > Double.NEGATIVE_INFINITY;
}
public Iterable pathTo(int v) {
if (!hasPathTo(v)) return null;
Stack stack = new Stack<>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
stack.push(e);
}
return stack;
}
}
4.4.6 一般加权有向图的最短路径问题
4.4.6.3 负权重的环
我们研究含有负权重边的有向图的时候,如果该图含有一个权重为负的环,那么最短路径的概念就失去意义了。例如图4.4.20,除了
5->4的权重为-0.66外,它和第一个示例完全相同。这里,环4->7->5->4权重为0.37+0.28-0.66=-0.01
只要围着这个环兜圈子就能得到权重任意短的路径
定义:加权有向图的**负权重环**是一个总权重(环上所有边的权重之和)为负的有向环
无论是否存在负权重环,从s到可达的其他顶点的一条最短路径都是存在的。不幸的是,解决这个问题的最好方法在最坏情况下所需的时间是指数级别的。一般来说,我们认为这个问题“太难了”。
现在我们解决下列问题:
- 负权重环的检测。给定的加权有向图中含有负权重环吗? 如果有,找到它。
- 负权重环不可达时的单点最短路径。给定一幅加权有向图和一个起点s且s无法到达任何负权重环,回答“是否存在一条从s到给定顶点v的有向路径?如果有,找出最短(总权重最小)的那条路径”。
总结尽管在含有环的有向图中最短路径没有意义,而且也无法解决在这种有向图中高效找出最短简单路径的问题,但在实际应用中仍需要识别出其中的负权重环。
我们不会仔细研究这个版本,因为它总是放松VE条边且只需要稍加修改即可使算法在一般的应用场景中更高效。
4.4.6.5 基于队列的Bellman-Ford算法
根据经验我们知道在任意一轮中许多边的放松都不会成功:只有上一轮中的distTo[]值发生变化的顶点指出的边才能够改变其他distTo[]元素的值。
为了记录这样的顶点,我们使用了一条FIFO队列。
4.4.6.6 实现
根据这些描述实现Bellman-Ford算法需要的代码非常少
基于下面两种数据结构:
- 一条用来保存即将被放松的顶点队列queue;
- 一个由顶点索引的boolean的数据onQ[],用来指示顶点是否存在队列中,以防止将顶点重复插入队列。
这些数据结构保证,队列中不出现重复的顶点,在某一轮中改变了edgeTo[]和distTo[]的所有顶点在下一轮中被处理。
算法 基于队列的Bellman-Ford算法
/**
* 基于队列的Bellman-Ford算法
*/
public class BellmanFordSP {
private double[] distTo; //从起点到某个顶点的路径长度
private DirectedEdge[] edgeTo; //从起点到某个顶点的最后一条边
private boolean[] onQ; //该顶点是否存在于队列中
private Queue queue; //正在被放松的顶点
private int cost; //relax()的调用次数
private Iterable cycle;//edgeTo[]中是否有负权重环
public BellmanFordSP(EdgeWeightedDigraph G, int s) {
distTo = new double[G.V()];
edgeTo = new DirectedEdge[G.V()];
onQ = new boolean[G.V()];
queue = new ArrayDeque<>();
for (int v = 0; v < G.V(); v++) {
distTo[v] = Double.POSITIVE_INFINITY;
}
distTo[s] = 0.0;
queue.add(s);
onQ[s] = true;
while (!queue.isEmpty() && !hasNegativeCycle()) {
int v = queue.remove();
onQ[v] = false;
relax(G, v);
}
}
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
if (!onQ[w]) {
queue.add(w);
onQ[w] = true;
}
}
if (cost++ % G.V() == 0) {
findNegativeCycle();
}
}
}
private void findNegativeCycle() {
int V = edgeTo.length;
EdgeWeightedDigraph spt;
spt = new EdgeWeightedDigraph(V);
for (int v = 0; v < V; v++) {
if (edgeTo[v] != null)
spt.addEdge(edgeTo[v]);
}
EdgeWeightedCycleFinder cf;
cf = new EdgeWeightedCycleFinder(spt);
cycle = cf.cycle;
}
private boolean hasNegativeCycle() {
return cycle != null;
}
}
public Iterable negativeCycle(){
return cycle;
}
4.4.6.7 负权重的边
4.4.6.8 负权重环的检测
检测负权重环来避免无限的循环。
- 添加一个变量cycle和一个私有函数findNegativeCycle()。如果找到负权重环,则会将cycle设为含有环中所有边的一个迭代器。
- 每调用V次relax()方法后即调用findNegativeCycle()方法。