最短路径

APIs

带权有向图中的最短路径,这节讨论从源点(s)到图中其它点的最短路径(single source)。

Weighted Directed Edge API

需要新的数据类型来表示带权有向边。

Weighted Directed Edge:implementation

public class DirectedEdge {
private final int v, w;
private final double weight; public DirectedEdge(int v, int w, double weight) {
this.v = v;
this.w = w;
this.weight = weight;
} public int from() {
return v;
} public int to() {
return w;
} publiv int weight() {
return weight;
}
}

习惯上处理边 e 的时候先获取端点:int v = e.from(), w = e.to();

Edge-weighted Digraph API

依然是使用邻接表表示,保存的是指向边对象的引用。

Edge-weighted Digraph:implementation

public class EdgeWeightedDigraph {
private final int V;
private final Bag<DirectedEdge>[] adj; public EdgeWeightedDigraph(int V) {
this.V = V;
adj = (Bag<DirectedEdge>[]) new Bag[V];
for (int v = 0; v < V; v++)
adj[v] = new Bag<DirectedEdge>();
} public void addEdge(DirectedEdge e) {
int v = e.from();
adj[v].add(e);
} public Iterable<DirectedEdge> adj(int V) {
return adj[v];
}
}

Single-source Shortest Paths API

测试用例

SP sp = new SP(G, s);
for (int v = 0; v <G.V(); v++) {
StdOut.printf("%d to %d (%.2f): ", s, v, sp.distTo(v));
for (DirectedEdge e : sp.pathTo(v))
StdOut.print(e + " ");
StdOut.println();
}

运行示例

注:上述内容,详细的可以参考 booksite-4.4

Shortest-paths Properties

Data Stuctures

这里讨论单点最短路径,我们用两个以点为索引的数组来表示最短路径树(SPT)。

edgeTo[i] 表示从点 0 到点 i 的最短路径上的最后一条边,用来还原最短路径,edgeTo[0] 记为 null。distTo[i] 即表示从点 0 到点 i 的最短路径长度,distTo[0] 为 0。

public double distTo(int v) {
return distTo[v];
} public Iterable<DirectedEdge> pathTo(int V) {
Stack<DirectedEdge> path = new Stack<DirectedEdge>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from])
path.push(e);
return path;
}

Relaxation

我们的最短路径 API 的实现都基于一个被称为松弛(relaxation)的简单操作。想象把一根橡皮筋沿最短路径拉长,找到更短的路径,也就“松弛”了这根橡皮筋。

Edge

放松边 v->w 就是要检查源点 s 到点 w 经过这条边是否会更短。

private void relax(DirectedEdge e) {
int v = e.from(), w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
}
}

下图中,左边例子称为边失效,右边则说放松是成功的。

Vertex

点的松弛即放松由其发出的所有边。

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;
}
}
}

Shortest-paths Optimality Conditions

最优性条件:当且仅当对于任意从点 v 到点 w 的边 e,满足 distTo[w]\(\leqslant\)distTo[v]+e.weight()(没有可以放松的有效边),那么 distTo[w] 是从 s 到 w 的最短路径长度。

证明

  • 必要性

    假设 distTo[w] 是 s 到 w 的最短路径。若存在边 e(v->w) 有 distTo[v]+e.weight()<distTo[w],显然从 s 到 v 再经 e 到 w 更短,矛盾。

  • 充分性

    假设 s=\(v_{0}\)->\(v_{1}\)->\(v_{2}\)->...->\(v_{k}\)=w 是 s 到 w 的最短路径,其权重记为 \(OPT_{sw}\),\(e_{i}\) 表示路径上的第 i 条边,有:

    distTo[\(v_{1}\)] \(\leqslant\) distTo[\(v_{0}\)] + \(e_{1}\).weight()

    distTo[\(v_{2}\)] \(\leqslant\) distTo[\(v_{1}\)] + \(e_{2}\).weight()

    ...

    distTo[\(v_{k}\)] \(\leqslant\) distTo[\(v_{k-1}\)] + \(e_{k}\).weight()

    综合这些不等式并去掉 distTo[\(v_{0}\)] = distTo[s] = 0:

    distTo[w] = distTo[\(v_{k}\)] \(\leqslant\) \(e_{1}\).weight() + \(e_{2}\).weight() + ... + \(e_{k}\).weight() = \(OPT_{sw}\)

    又因为 distTo[w] 是从 s 到 w 的某条路径的长度,不会比最短路径更短,所以下列式子成立。

    \(OPT_{sw} \leqslant\) distTo[w] \(\leqslant OPT_{sw}\)

Generic Shortest-paths Algorithm

由上述最优性条件马上可以得到一个计算单点最短路径问题的 SPT 的通用算法:

  • 将 distTo[s] 初始化为 0,其它 distTo[] 元素初始化为无穷大。
  • 重复放松图 G 中的任意边,直到不存在有效边为止(满足最优性条件)。

Pf

  • 算法会把 distTo[v] 赋值成某条从 s 到 v 的路径长,且 edgeTo[v] 是该路径的最后一条边。
  • 对于 s 可到达的点 v,distTo[v]初始为无穷大,肯定存在有效边。
  • 每次成功的放松都会减少某些 distTo[v],distTo[v] 减少的次数是有限的。

:暂时不考虑负权重。

通用算法没有指定边放松的顺序,它为我们提供了证明算法可以计算 SPT 的方式:证明算法会放松所有边直到没有有效边。

Dijkstra's Algorithm

Dijkstra 算法采用和 Prim 算法构建 MST 类似的策略来构建 SPT:每次添加的都是离起点最近的非树顶点。

从起点 s(0) 开始,维护两个数组 distTo 和 edgeTo 来表示 SPT,先把 distTo[0] 置为 0.0,其它 distTo 元素置为无穷大,edgeTo[0] 置为 null。

最初 distTo 数组中最小的非树点(离 SPT 最近的非树顶点)即是 distTo[0],把点 0 加入 SPT 并进行放松操作。其它 distTo 被初始化为无穷大,点 0 发出的都是有效边,更新对应的 distTo 和 edgeTo 元素。

现在 distTo 数组中最小的是非树顶点是 distTo[1](显然还需要索引优先队列来帮我们快速获取离 SPT 最近的非树顶点),加入 SPT 并放松点 1。distTo[2] 和 distTo[3] 初始更新,边 1-7 无效。

现在点 7 是离 SPT 最近的非树点,加入 SPT 并放松,边 2-7 有效,更新 distTo[2],edgeTo[2] 变为 7->2。

每次挑离 SPT 最近的非树点加入 SPT 并进行放松操作,直到可到达的点都被加入 SPT,也就完成了计算。

s 可到达的点都只会被放松一次,当 v 被放松时,有 distTo[w]\(\leqslant\)distTo[v]+e.weight(),而且该不等式在算法结束前都会成立,因为:

  • distTo[w] 不会增加。因为放松操作只有可能减少 distTo[w]。
  • distTo[v] 不会改变。边的权重非负,我们每次选择的又都是最小的 distTo[] 值,后面的放松操作不可能使任何 distTo[] 的值小于 distTo[v]。

满足最优性条件,所以 Dijkstra 算法可以解决边权重非负的加权有向图的单点最短路径问题。

Dijkstra: Java Implementation

public class DijkstraSP {
private DirectedEdge[] edgeTo;
private double[] distTo;
private IndexMinPQ<Double> pq; public DijkstraSP(EdgeWeightedDigraph G, int s) {
edgeTo = new DirectedEdge[G.V()];
distTo = new double[G.V()];
pq = new IndexMinPQ<Double>(G.V()); for (int v = 0; v < G.V(); v++)
distTo[v] = Double.POSITIVE_INFINITY;
distTo[s] = 0.0; pq.insert(s, 0.0);
while (!pq.isEmpty()) {
int v = pq.delMin();
for (DirectedEdge e : G.adj(v))
relax(e);
}
} private void relax(DirectedEdge e) {
int v = e.from(), w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
if (pq.contains(w)) {
pq.decreaseKey(w, distTo[w]);
} else {
pq.insert(w, distTo[w]);
}
}
}
}

时间复杂度取决于优先队列的实现,二叉堆的话是 \(ElogV\) 级别。

Edge-weighted DAGs

对于带权无环有向图(DAG)我们有比 Dijkstra 更简单的算法:按图的拓扑排序来放松点,它能在线性时间内计算 SPT,能处理负权重,还可以用来找出最长路径。

直接按拓扑排序放松点,最终的 SPT 和 Dijkstra 算法跑的一样,还不需要优先队列,时间复杂度是 \(E+V\) 级别。

证明其正确性也和 Dijkstra 类似,它也会满足最优性条件:

  • 每条边 e(v->w) 只会被放松一次(放松点 v 时),然后有不等式:distTo[w]\(\leqslant\)distTo[v]+e.weight()。

  • 不等式在算法结束前都会成立,因为:

    1. distTo[w] 不会增加,因为放松只可能减少 distTo[] 的值。
    2. distTo[v] 不会改变,因为按拓扑顺序放松,指向点 v 的边不会在点 v 被放松之后放松。
  • 因此,算法结束时满足最短路径的最优性条件,可以正确计算 SPT。

public class AcyclicSP {
private DirectedEdge[] edgeTo;
private double[] distTo; public AcylicSP(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 topological = new Topological(G);
for (int v : topo;ogical.order())
for (DirectedEdge e : G.adj(v))
relax(e);
}
}

这种处理无环有向图最短路径问题的算法,可以被用于调整图片大小,且图片不会失真。

把图片的像素点当做图的点,每个点和下层的三个临近点相连,能量函数根据像素点周围的八个点计算其权重,调整大小时就把最短路径(路径上点权重总和最小)上的像素点去掉。

Longest Paths

拓扑排序算法可以处理负权重边(Dijkstra 要非负才能满足最优性条件),那么我们把边权重取负后再跑,得到的就是最长路径。

最长路径可以应用于平行任务调度问题。

这个并行任务调度有优先级的限制,比如任务 0 必须在任务 1 之前完成,实际应用中很常见,像你要装好车门才能喷漆。

将任务调度抽象成带权无环有向图,每个任务预计开始时间即为从起点到它的起始顶点的最长距离,而图的最长路径 0->9->6->8->2 即并行任务调度问题的关键路径(完成所有任务的最早可能时间)。

Negative Weights

Dijkstra 算法不能处理负权重边,它会直接选择当前最短的边,而不会绕远路去走负权重的边。所以下图到点 3 的最短距离直接会是边 0->3 的权重 2,而不是实际上的 0->1->2->3 的总权重 1。

一个可能的尝试是把边权重都加上同一常数,让权重非负,但这样还是会改变最短路径,像上图那样。

介绍能处理负权重边的算法之前,要有个概念:当且仅当图不存在负权重环(环的总权重小于 0)时,SPT 存在。

存在负权重环,最短路径可能一直减少下去。

Bellman-Ford

算法步骤

  1. 将 distTo[s] 初始化为 0,其它 distTo[] 元素初始化为无穷大。
  2. 重复 V 次:
    • 放松每条边。
for (int i = 0; i < G.V(); i++)
for (int v = 0; v < G.V(); v++)
for (DirectedEdge e : G.adj(v))
relax(e);

Bellman-Ford 算法能够计算任意不含负权重环的带权有向图的 SPT,所需时间正比于 \(E
\times V\)。

证明

在没有负权重环的带权有向图中,对于源点 s 可到达的点 t,会存在最短路径,不妨记为:s=\(v_{0}\)->\(v_{1}\)->...->\(v_{k}\)=t,显然 k \(\leqslant\)V-1。证明算法正确性的等价命题:算法在第 i 轮之后能得到 s 到 \(v_{i}\) 的最短路径 \(v_{0}\)->\(v_{1}\)->...->\(v_{i}\)。

  • 对于 i=0,显然成立。

  • 假设算法在第 i 轮之后能得到 s 到 \(v_{i}\) 的最短路径,即为 distTo[\(v_{i}]\)。

  • 第 i+1 轮放松后,distTo[\(v_{i+1}\)]=distTo[\(v_{i}\)]+(\(v_{i}\)->\(v_{i+1}\)).weight(),因为:

    1. 每轮放松所有点,\(v_{i}\) 被放松后,distTo[\(v_{i+1}\)] 不会大于等式右边。
    2. 右边即最短路径 \(v_{0}\)->\(v_{1}\)->...->\(v_{i+1}\) 长度,也不会比它还小。

    所以在第 i+1 轮放松后,算法能够得到从 s 到 \(v_{i+1}\) 的最短路径。

改进

如果 distTo[v] 在第 i 轮放松中没有改变,那么在第 i+1 轮中也就没有必要放松点 v。于是我们可以维护一个队列,保存 distTo[] 发生改变的那些点,下一轮只放松它们就好。为了防止重复放入,再来个布尔数组表示点是否已经在队列中。所以 Bellman-Ford 算法最坏情况下需要正比于 \(E \times V\) 的时间,但实际使用时一般会快很多。

Cost Summary

Find A Negative Cycle

如果存在负权重环的话,基于队列实现的 Bellman-Ford 算法会陷入死循环,因为第 i 轮放松后找到的最短路径最多只有 i 条边,所以有必要实现检测负权重环的方法。

如果图有负权重环,那么 edgeTo[] 还原出来的 SPT 就会有环,于是每一轮放松之后,就检测一下 SPT 是否有环。

private void findNegativeCycle() {
int V = edgeTo.length;
EdgeWeightedDigraph spt = new EdgeWeightedDigraph(V);
for (int v = 0; v < V; v++)
if (edgeTo[v] != null)
spt.addEdge(edgeTo[v]); EdgeWeightedDirectedCycle cf = new EdgeWeightedDirectedCycle(spt);
cycle = cf.cycle();
} public boolean hasNegativeCycle() {
return cycle != null;
} public Iterable<Edge> negativeCycle() {
return cycle;
}

实现用到了之前课程里检测环的类,详细的参见:BellmanFordSP.java,或是 booksite-4.4,不提。

Application

负权重环检测可以被应用于套汇。

1000 美元可以换 741 欧元,后者再换成 1012.206 = 741 \(\times\) 1.366 加元,加元再换回美元变成 1012.206 \(\times\) 0.995 = 1007.14497,也就赚了 7.14497 美元。

在这样的图里寻找权重乘积(\(w_{1}w_{2}...w_{k}\))大于 1 的路径,等价于 -ln(\(w_{1}\))-ln(\(w_{2}\))-...-ln(\(w_{k}\)) 小于零,于是我们把权重取对数再取反,找到的负权重环即目标路径。

Shortest Paths的更多相关文章

  1. Codeforces 1005 F - Berland and the Shortest Paths

    F - Berland and the Shortest Paths 思路: bfs+dfs 首先,bfs找出1到其他点的最短路径大小dis[i] 然后对于2...n中的每个节点u,找到它所能改变的所 ...

  2. Codeforces Round #496 (Div. 3) F - Berland and the Shortest Paths

    F - Berland and the Shortest Paths 思路:还是很好想的,处理出来最短路径图,然后搜k个就好啦. #include<bits/stdc++.h> #defi ...

  3. 【例题收藏】◇例题·II◇ Berland and the Shortest Paths

    ◇例题·II◇ Berland and the Shortest Paths 题目来源:Codeforce 1005F +传送门+ ◆ 简单题意 给定一个n个点.m条边的无向图.保证图是连通的,且m≥ ...

  4. CSU 1506 Double Shortest Paths

    1506: Double Shortest Paths Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 49  Solved: 5 Description ...

  5. CF Gym 102028G Shortest Paths on Random Forests

    CF Gym 102028G Shortest Paths on Random Forests 抄题解×1 蒯板子真jir舒服. 构造生成函数,\(F(n)\)表示\(n\)个点的森林数量(本题都用E ...

  6. All shortest paths between a set of nodes

    .big{font-size:larger} .small{font-size:smaller} .underline{text-decoration:underline} .overline{tex ...

  7. [Codeforces 1005F]Berland and the Shortest Paths(最短路树+dfs)

    [Codeforces 1005F]Berland and the Shortest Paths(最短路树+dfs) 题面 题意:给你一个无向图,1为起点,求生成树让起点到其他个点的距离最小,距离最小 ...

  8. UVA 12821 Double Shortest Paths

    Double Shortest PathsAlice and Bob are walking in an ancient maze with a lot of caves and one-way pa ...

  9. Codeforces Round #Pi (Div. 2) 567E President and Roads ( dfs and similar, graphs, hashing, shortest paths )

    图给得很良心,一个s到t的有向图,权值至少为1,求出最短路,如果是一定经过的边,输出"YES",如果可以通过修改权值,保证一定经过这条边,输出"CAN",并且输 ...

随机推荐

  1. Objekt Orientierte Programmierung C++

    1.Funtion Overloading C++ erlaubt,dass einige Funktion gleiches Names deklariert wird.Der Formale Pa ...

  2. Gin实战:Gin+Mysql简单的Restful风格的API

    我们已经了解了Golang的Gin框架.对于Webservice服务,restful风格几乎一统天下.Gin也天然的支持restful.下面就使用gin写一个简单的服务,麻雀虽小,五脏俱全.我们先以一 ...

  3. MySQL中使用SHOW PROFILE命令分析性能的用法整理(配合explain效果更好,可以作为优化周期性检查)

    这篇文章主要介绍了MySQL中使用show profile命令分析性能的用法整理,show profiles是数据库性能优化的常用命令,需要的朋友可以参考下   show profile是由Jerem ...

  4. 使用 Flask 框架写用户登录功能的Demo时碰到的各种坑(二)——使用蓝图功能进行模块化

    使用 Flask 框架写用户登录功能的Demo时碰到的各种坑(一)——创建应用 使用 Flask 框架写用户登录功能的Demo时碰到的各种坑(二)——使用蓝图功能进行模块化 使用 Flask 框架写用 ...

  5. C# 实现将listview中已经显示的数据导出到Access 数据库

    private void button1_Click(object sender, EventArgs e) { OleDbConnection dbconn = new OleDbConnectio ...

  6. 使用http维持socket长连接

    项目中有遇到问题如下: 1.旧版的cs服务,因为每个用户和唯一的长连接是在登录后绑定的,并且所有的消息报文均是基于该长连接去发送接收的,所以要求node服务要维持一个长连接,然后根据该用户获取长连接, ...

  7. PhpStorm 破解及 XDebug 调试

    PhpStorm 破解及 XDebug 调试 PhpStorm 破解 PhpStorm 10.0.2 破解 地址:http://jingyan.baidu.com/article/20095761cb ...

  8. 六、yarn运行模式

    简介 spark的yarn运行模式根据Driver在集群中的位置分成两种: 1)yarn-client 客户端模式 2)yarn-cluster 集群模式 yarn模式和standalone模式不同, ...

  9. 01 使用JavaScript原生控制div属性

    写在前面: 因对前端开发感兴趣,于是自学前端技术,现在已经会HTML.CSS.JavaScript基础技术.但水平处于小白阶段,在网上找一些小项目练练手,促进自己的技术成长.文章记录自己的所思所想,以 ...

  10. 以面向对象的思想实现数据表的添加和查询,JDBC代码超详细

    以面向对象的思想编写JDBC程序,实现使用java程序向数据表中添加学生信息,并且可以实现给定身份证号查询学生信息或给定准考证号查询学生信息. 创建的数据表如下: CREATE TABLE EXAMS ...