一、问题介绍
1.求K条最短路径的必要性
最短路径问题分为:
- 单源最短路径
- 所有顶点对间的最短路径
共同的缺陷:
这里的最短路径指两点间最短的那一条路径,不包括次短、再次短等路径。这样的最短路径问题比较狭义。
在实际情况中,例如:用户在使用咨询系统或决策支持系统时,希望得到最优的决策参考外,还希望得到次优、再次优等决策参考,这同样反映在最短路径问题上。因此有必要将最短路径问题予以扩展和延伸,称为K条最短路径问题,即不但要求得最短路径,还要求得次短、再次短等路径。
2.KSP问题定义
KSP问题是对最短路径问题的推广,它除了要确定最短路径之外,还要确定次短路径、第三短路径,...,知道找到第K短路径。用Pi表示从起点s到终点t的第i短路径,KSP问题是确定路径集合Pk={p1,p2,p3,...,pk},使得满足以下3个条件:
1)K条路径是按次序产生的,即对于所有的i(i=1,2,...,K-1)
,pi是在pi+1之前确定;
2)K条路径是按长度从小到大排列的,即对于所有的i(i=1,2,...,K-1)
,都有c(pi)<c(pi+1)
;
3)这K条路径是最短的,即对于所有的p∈Pst-PK
,都有c(pk)<c(p)
。
3.相关概念
设起点为 1 ,终点为 5 。p1、p2、p3 分别表示起点到终点间最短路径、第二短路径、第三短路径。
1)偏离点
p3
相对于p1
的偏离节点为节点 2
2)偏离边
p3
相对于p1
的偏离边为(2,4)
3)偏离路径
p3
相对于p1
的(2,4,5)
4.KSP算法分类
1) 一般KSP问题
对路径没有任何限制
2) 限定无环KSP问题
要求所有路径都是简单路径,不能有环
当路径中所有节点都不同时,称为无环路径
两类KSP问题具有不同的特征,分别提出了不同的求解算法,分别称之为
- 一般的KSP算法
- 限定无环KSP算法
二、思路
下面列出用Yen's算法求KSP的代码。该算法是Yen在1971年提出的以其名字命名的Yen算法。算法采用了递推法中的偏离路径算法思想,适用于非负权边的有向无环图。
1.流程图
Q:将什么样的点做为偏离点
在
pk
的基础上求pk+1
时,将pk
的路径上除终点d
之外的节点分别作为偏离点
Q:求Vi到终点d的最短路径
设起点为s,终点为t,偏离点为Vi
。求偏离点到终点的最短路径时要注意两个问题
- 防止从起点到终点的整体路径有环
从Vi到t的最短路径
不能包含s到Vi的路径
上的任何节点 - 避免与已经在结果列表中的路径重复
从Vi发出的边
不能与结果列表中的路径p1,p2,...pk,上从Vi发出边
的相同
这个体现在代码上就是:在用Dijkstra算法算最短路径时对数组的初始化要进行特别处理
// 不能走的边
if (unavailableEdges != null && unavailableEdges.size() != 0) {
for (MyPath p: unavailableEdges) {
int index = p.path.indexOf(startIndex);
if (index >= 0 && (index + 1) >= 0) {
dist[index + 1] = Double.MAX_VALUE;
path[index + 1] = -1;
}
}
}
// 不能走的点
if (unavailableNodeIndexs != null && unavailableNodeIndexs.size() != 0) {
for (Integer point: unavailableNodeIndexs) {
set[point] = 1;
}
}
Q:每次找到新的最短路径时,需将该路径从候选链列表中移除,并加入到结果列表中
Q:候选列表中的路径不能重复
所以选择用
Set
来存储候选路径,去重
Q:如何从候选列表中选出合适的路径
如果只有一条路径权值和最小的路:将该路径返回;
如果路径权值和最小的路有多条:从其中选出节点数最少的路径。
2.手工模拟求K最短路径流程
求C到H的10条最短路径
节点加载内存后存储在Node[] nodes
数组中,各个节点在数组中的存储位置如下,下面用各个点的数组下标进行说明。表格中括号中备注为路径权重。
1)通过最短路径算法Dijkstra得到C到H的最短路径
p1=C-E-F-H
,即0-2-3-5
2)在p1
的基础上求p2
遍历完各个偏离点后的情况:
从集合B中选出路径0->2->4->5(7)
移除,并加入到集合A中,作为p2
3)在p2
的基础上求p3
遍历完各个偏离点后的情况:
从集合B中选出路径0->1->3->5(8)
移除,并加入到集合A中,作为p3
4)在p3
的基础上求p4
遍历完各个偏离点后的情况:
从集合B中选出路径0->1->3->5(8)
移除,并加入到集合A中,作为p4
5)在p4
的基础上求p5
遍历完各个偏离点后的情况:
从集合B中选出路径0->2->3->4->5(8)移除,并加入到集合A中,作为
p5`
6)在p5
的基础上求p6
遍历完各个偏离点后,没有新的候选路径加入集合B中
从集合B中选出路径0->1->3->4->5(11)
移除,并加入到集合A中,作为p6
7)在p6
的基础上求p7
遍历各个偏离点时求最短路径均不存在,故遍历完各个偏离点后,没有新的候选路径加入集合B中,从集合B中选出路径0->2->1->3->4->5(11)
移除,并加入到集合A中,作为p7
8)在p7
的基础上求p8
遍历各个偏离点时求最短路径均不存在,故遍历完各个偏离点后,没有新的候选路径加入集合B中,候选集合为空,不能再继续向下寻找。故从C到H共有7条路径。
三、代码
1.输入上述图
6 9
11 C
12 D
13 E
14 F
15 G
16 H
11 12 3
11 13 2
12 14 4
13 12 1
13 14 2
13 15 3
14 15 2
14 16 1
15 16 2
11 16 10
2.代码
package road;
import util.NavigationUtil;
import java.util.*;
/**
* <pre>
* author : 杨丽金
* time : 2018/11/14
* desc : 有关于图的最短路径
* version: 1.0
* </pre>
*/
public class ShortestPath {
public static class MyPath {
// 路径上的各个节点对应的数组下标(从起点到终点)
public List < Integer > path;
// 路径总权值
public double weight;
// 路径上节点个数:通过path.size()得到
public MyPath() {}
public MyPath(List < Integer > path, double weight) {
this.path = path;
this.weight = weight;
}
@Override
public String toString() {
return "MyPath{" +
"path=" + path +
", weight=" + weight +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyPath path1 = (MyPath) o;
return path != null ? path.equals(path1.path) : path1.path == null;
}
@Override
public int hashCode() {
int result;
long temp;
result = path != null ? path.hashCode() : 0;
temp = Double.doubleToLongBits(weight);
result = 31 * result + (int)(temp ^ (temp >>> 32));
return result;
}
}
/**
* 用Yen's KSP算法从图中找出从startIndex到endIndex的K条最短路径
*
* @param g
* @param startIndex:起始节点的数组下标
* @param endIndex:终止节点的数组下标
* @param K:要求的最短路径数目
* @return
*/
public List < MyPath > KSP_Yen(MyGraph g, int startIndex, int endIndex, int K) {
// 结果列表
List < MyPath > result = new ArrayList < > ();
// 候选路径列表
Set < MyPath > candidatePaths = new HashSet < > ();
// 候选路径列表中权值最小的路径,及其对应的节点个数
// 第一条最短路径
MyPath p1 = getSingleShortestPath_dijkstra(g, startIndex, endIndex, null, null);
result.add(p1);
int k = 1;
List < Integer > pk = p1.path;
while (k < K) {
/*
求第k+1条最短路径
*/
// 遍历每一个偏离点
for (int i = 0; i <= pk.size() - 2; i++) {
// 1,pk路径中起点到偏离点Vi的路径权值
double w1 = 0;
for (int j = 0; j <= i - 1; j++) {
w1 += NavigationUtil.getEdgeWight(g, pk.get(j), pk.get(j + 1));
}
// 2,偏离点到终点的最短路径
MyPath viToDestinationSP = getSingleShortestPath_dijkstra(g,
pk.get(i), endIndex, pk.subList(0, i), result);
if (viToDestinationSP != null) {
// 说明从这个偏离点可以到达终点
MyPath temp = new MyPath();
List < Integer > tempPath = new ArrayList < > (pk.subList(0, i));
tempPath.addAll(viToDestinationSP.path);
temp.path = tempPath;
temp.weight = w1 + viToDestinationSP.weight;
// 加入候选列表
if (!candidatePaths.contains(temp)) {
candidatePaths.add(temp);
}
}
}
if (candidatePaths == null || candidatePaths.size() == 0) {
// 没有候选路径,则无需再继续向下求解
break;
} else {
// 从候选路径中选出最合适的一条,移除并加入到结果列表
MyPath fitPath = getFitPathFromCandidate(candidatePaths);
candidatePaths.remove(fitPath);
result.add(fitPath);
k++;
pk = fitPath.path;
}
}
return result;
}
/**
* 从候选列表中得到一条路径作为pk+1
* 要求:1)该路径的权值和最小;2)路径经过节点数最少
* @param candidatePaths:候选列表
* @return
*/
private MyPath getFitPathFromCandidate(Set < MyPath > candidatePaths) {
MyPath result = new MyPath(null, Double.MAX_VALUE);
for (MyPath p: candidatePaths) {
// 对于每一条路径
if (p.weight < result.weight) {
result = p;
}
if (p.weight == result.weight && p.path.size() < result.path.size()) {
result = p;
}
}
return result;
}
/**
* 用Dijkstra算法得到从startIndex到endIndex的一条最短路径
*
* @param g
* @param startIndex 起始节点的数组下标
* @param endIndex 终止节点的数组下标
* @param unavailableNodeIndexs:求最短路径时不可用的节点(数组下标)
* @param unavailableEdges:求最短路径时不可用的边
* @return
*/
public MyPath getSingleShortestPath_dijkstra(MyGraph g, int startIndex, int endIndex,
List < Integer > unavailableNodeIndexs, List < MyPath > unavailableEdges) {
if (startIndex == -1) {
// throw new Exception("getSingleShortestPath_dijkstra()起始点编号输入错误");
}
if (endIndex == -1) {
// throw new Exception("getSingleShortestPath_dijkstra()终止点编号输入错误");
}
int[] set = new int[g.n]; // 是否已并入集合,该点是否已找到最短路径
// s到i的最短路径长度
double[] dist = new double[g.n];
// s到i的最短路径上i的前一个节点编号
int[] path = new int[g.n];
// 初始化数组
set[startIndex] = 1;
for (int i = 0; i < g.n; i++) {
if (i == startIndex) { // 源点
dist[i] = 0;
path[i] = -1;
} else {
if (NavigationUtil.isConnected(g, startIndex, i)) {
dist[i] = NavigationUtil.getEdgeWight(g, startIndex, i);
path[i] = startIndex;
} else {
dist[i] = Double.MAX_VALUE;
path[i] = -1;
}
}
}
// 不能走的边
if (unavailableEdges != null && unavailableEdges.size() != 0) {
for (MyPath p: unavailableEdges) {
int index = p.path.indexOf(startIndex);
if (index >= 0 && (index + 1) >= 0) {
dist[p.path.get(index + 1)] = Double.MAX_VALUE;
path[p.path.get(index + 1)] = -1;
}
}
}
// 不能走的点
if (unavailableNodeIndexs != null && unavailableNodeIndexs.size() != 0) {
for (Integer point: unavailableNodeIndexs) {
set[point] = 1;
}
}
// 需进行n-2轮循环
for (int i = 0; i < g.n - 2; i++) {
int k = -1;
double min = Double.MAX_VALUE;
// 找出dist[]中最小的(太贪心了)
for (int j = 0; j < g.n; j++) {
if (set[j] == 1) {
continue;
}
if (dist[j] < min) {
min = dist[j];
k = j;
}
}
if (k == -1) {
// 说明从源点出发与其余节点不连通,无法再向下进行扩展
break;
}
set[k] = 1; // 把节点k并入
// 修改dist[]、path[]
for (int j = 0; j < g.n; j++) {
if (set[j] == 1) {
continue;
}
if (NavigationUtil.isConnected(g, k, j)) {
double temp = dist[k] + NavigationUtil.getEdgeWight(g, k, j);
if (temp < dist[j]) {
dist[j] = temp;
path[j] = k;
}
}
}
}
System.out.println("运行Dijkstra算法后的数组情况为:");
System.out.print("set[]:");
for (int i = 0; i < g.n; i++) {
System.out.print(set[i] + "\t");
}
System.out.println();
System.out.print("dist[]:");
for (int i = 0; i < g.n; i++) {
System.out.print(dist[i] + "\t");
}
System.out.println();
System.out.print("path[]:");
for (int i = 0; i < g.n; i++) {
System.out.print(path[i] + "\t");
}
System.out.println();
// 输出
if (dist[endIndex] == Double.MAX_VALUE) {
// 说明没有最短路径,两点不连通
System.out.println("两点之间不连通");
return null;
} else {
System.out.println("节点" + g.nodes[startIndex].nodeId + "到节点" +
g.nodes[endIndex].nodeId + "的最短路径长度为:" + dist[endIndex] + ",具体路径是:");
MyPath result = new MyPath();
result.path = getMinimumPath(g, startIndex, endIndex, path);
result.weight = dist[endIndex];
return result;
}
}
/**
* 输出从节点S到节点T的最短路径
*
* @param sIndex:起始节点在数组中下标
* @param tIndex:终止节点在数组中下标
*/
private List < Integer > getMinimumPath(MyGraph g, int sIndex, int tIndex, int[] path) {
List < Integer > result = new ArrayList < > ();
Stack < Integer > stack = new Stack < > ();
stack.push(tIndex);
int i = path[tIndex];
while (i != -1) {
stack.push(i);
i = path[i];
}
while (!stack.isEmpty()) {
System.out.print(g.nodes[stack.peek()].nodeId + ":" + g.nodes[stack.peek()].nodeName + "-->");
result.add(NavigationUtil.getIndex(g, g.nodes[stack.pop()].nodeId));
}
System.out.println();
return result;
}
}
3.输出
sps: [MyPath {
path = [0, 2, 3, 5], weight = 5.0
}, MyPath {
path = [0, 2, 4, 5], weight = 7.0
}, MyPath {
path = [0, 1, 3, 5], weight = 8.0
}, MyPath {
path = [0, 2, 1, 3, 5], weight = 8.0
}, MyPath {
path = [0, 2, 3, 4, 5], weight = 8.0
}, MyPath {
path = [0, 1, 3, 4, 5], weight = 11.0
}, MyPath {
path = [0, 2, 1, 3, 4, 5], weight = 11.0
}]
参考文献
[1]K条最短路径问题综述
[2]韩海玲. 基于城市路网的最短路径算法研究与应用[D]. 中北大学, 2017.
[3]徐涛, 丁晓璐, 李建伏. K最短路径算法综述[J]. 计算机工程与设计, 2013, 34(11):3900-3906.
[4]K条最短路径算法(KSP, k-shortest pathes):Yen's Algorithm