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閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容