最短路径
最小生成树是以无向带权图为基准,而最短路径则是以加权有向图
为基准
最短路径树
- 给定一幅加权有向图和一个顶点s。
- 以s为起点的一棵最短路径树是图的一幅子图,它包含s和从s可达的所有顶点。
- 根节点为s,到叶子结点的每条路径和都是有向图中的一条最短路径
基本数据结构:
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;
}
}
public class EdgeWeightedDigraph {
// 通过始点来记录边
private final int V;
private int E;
private Bag<DirectedEdge>[] adj;
public EdgeWeightedDigraph(int V) {
this.V = V;
this.E = 0;
adj = (Bag<DirectedEdge>[]) 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<DirectedEdge> adj(int v){
return adj[v];
}
public Iterable<DirectedEdge> edges(){
Bag<DirectedEdge> bag = new Bag<>();
for (int v = 0; v < V; v++) {
for (DirectedEdge edge : adj[v]) {
bag.add(edge);
}
}
return bag;
}
}
数据结构记录:
-
最短路径树中的边:edgeTo[],edgeTo[v]的值为树中连接v和它的父节点的边(也是从s到v的最后一条边)
-
到达起点的距离。distTo[],其中distTo[],为从s到v的已知最短路径的长度
-
边的松弛:
- 检查从s→w的最短路径是否先从s→v,然后再由v→w
- 检查distTo[v]与e.weight()的和,如果这个值不小于distTo[w],则称这条边失效了,并将它忽略
- 如果这个值更小,就更新内容
private void relax(DirectedEdge e){
int v = e.from(), w = e.to();
if(distTo[w] > distTo[v] + e.weight()){
// 相当于s→v的距离和v到w的边的权值和更小 -- 比之前的直接从s→w更小
distTo[w] = distTo[v] + e.weight();
// 将w的边替换为当前的e
edgeTo[w] = e;
}
}
通用算法:
- 放松G中的任意边,知道不存在有效边为止。对于任意从s可达的顶点w,在进行这些操作后,distTo[w]的值即为从s到w的最短路径长度
Dijkstra算法
算法流程:
- 首先将distTo[s]初始化为0,distTo[]中的其他元素初始化为正无穷,然后将distTo[]最小的非树顶点放松并加入树中
- 知道所有的顶点都在树中或者所有的非树顶点的distTo[]值均为无穷大
准备条件
- distTo[] edgeTo[]数组
- 索引优先队列pq;临时结点 – 保存需要被放松的顶点或者确认下一个被放松的节点
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<>(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()) {
// 放松pq权值最小的点
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.changeKey(w, distTo[w]);
else pq.insert(w, distTo[w]);
}
}
}
public double distTo(int v) {
validateVertex(v);
return distTo[v];
}
public boolean hasPathTo(int v) {
validateVertex(v);
return distTo[v] < Double.POSITIVE_INFINITY;
}
public Iterable<DirectedEdge> pathTo(int v) {
validateVertex(v);
if (!hasPathTo(v)) return null;
Stack<DirectedEdge> path = new Stack<>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
path.push(e);
}
return path;
}
// 验证点是否在树中
private void validateVertex(int v) {
int V = distTo.length;
if (v < 0 || v >= V)
throw new IllegalArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
}
}
无环加权有向图的最短路径算法
问题变化:要求有权无向图的最短路径,相当于创建一个有向图,每个结点对于相邻结点都有一个双向箭头
- 核心:
- 在有向图的拓扑排序(保证前面的点都指向后面的点)中,对每一条边进行一次relax松弛操作。
- 松弛不改变distTo[v],即s→v的值不再改变,保证v→w的是最小值
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就等价于marked[v] = false
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;
}
}
}
}
拓展:求最长路径
- 所有的double最开始记为最小
- 松弛的判定变为
distTo[w] < distTo[v] + e.weight()
,由原来的大于变成小于
一般加权有向图中的最短路径问题
Bell–ford算法过程:
-
在任意含有V个顶点的加权有向图中给定起点s,s不可达到负权环(各边权重总和为负)
-
将distTo[s]设置为0,distTo[v]设置为无穷大,进行v轮边的放松,得到的即是环的最短路径
-
边的总数为V-1,所以通过V轮迭代后,再进行迭代,总会得到一个相同的结果。结果从第V轮开始收敛
-
只有上一轮中distTo[v]发生变化的边,下一轮才能改变其他元素的distTo[]值。通过一个队列来保存这样的点
-
辅助数据结构:
- 队列:保存即将被放松的顶点的队列
- 数组onQ[]:用来指示顶点是否已经存在于队列中,防止将顶点重复插入队列
- 可以保证:
- 队列不出现重复的点
- 在某一轮中,改变了distTo[]和edgeTo[]的值的所有顶点都会在下一轮处理
- 注意需要在检测到负权重环的时候退出
public class BellmanFordSP {
private double[] distTo;
private DirectedEdge[] edgeTo;
private boolean[] onQ;
// 正在被放松的顶点
private Queue<Integer> queue;
// relax的调用次数
private int cost;
// 检测是否有负权重环
private Iterable<DirectedEdge> cycle;
public BellmanFordSP(EdgeWeightedDigraph G, int s) {
distTo = new double[G.V()];
edgeTo = new DirectedEdge[G.V()];
onQ = new boolean[G.V()];
queue = new LinkedList<>();
//初始化各边,以进行放松操作
for (int v = 0; v < G.V(); v++) {
distTo[v] = Double.POSITIVE_INFINITY;
}
distTo[s] = 0.0;
// 将起点入队,只要入队,设置onQ为true
queue.offer(s);
onQ[s] = true;
// 队列非空,且不含有负权重环 -- 不然会陷入无限循环
while (!queue.isEmpty() && !hasNegativeCycle()){
int v = queue.poll();
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;
// 如果发生了变化,那么将这个结点入队
// 其他distTo不发生变化的结点以后也不会改变
if(!onQ[w]){
queue.offer(w);
onQ[w] = true;
}
}
if(cost++ % G.V() == 0){
// 当遍历的次数达到V时,进行负权重的检测
findNegativeCycle();
}
}
}
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<DirectedEdge> negativeCycle(){
return cycle;
}
}
// 环检测算法补充、
public EdgeWeightedDirectedCycle(EdgeWeightedDigraph G) {
marked = new boolean[G.V()];
onStack = new boolean[G.V()];
edgeTo = new DirectedEdge[G.V()];
for (int v = 0; v < G.V(); v++)
if (!marked[v]) dfs(G, v);
// check that digraph has a cycle
assert check();
}
// check that algorithm computes either the topological order or finds a directed cycle
private void dfs(EdgeWeightedDigraph G, int v) {
onStack[v] = true;
marked[v] = true;
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
// short circuit if directed cycle found
if (cycle != null) return;
// found new vertex, so recur
else if (!marked[w]) {
edgeTo[w] = e;
dfs(G, w);
}
// trace back directed cycle
// 到这里检测到了环,开始入栈进行回溯
else if (onStack[w]) {
cycle = new Stack<>();
DirectedEdge f = e;
while (f.from() != w) {
// 将点的出边先入栈
cycle.push(f);
// 再获取点的入边,直到没有入边为止,即到了环的起点
f = edgeTo[f.from()];
}
cycle.push(f);
return;
}
}
onStack[v] = false;
}
转载:https://blog.csdn.net/qq_41989109/article/details/106901823
查看评论