Yen的K条最短路径算法(KSP)

一、问题介绍

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算法
K最短路径算法分类

二、思路

下面列出用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容