[LeetCode] 847. Shortest Path Visiting All Nodes 訪問全部結點的最短路徑



An undirected, connected graph of N nodes (labeled 0, 1, 2, ..., N-1) is given as graph.html

graph.length = N, and j != i is in the list graph[i] exactly once, if and only if nodes i and j are connected.node

Return the length of the shortest path that visits every node. You may start and stop at any node, you may revisit nodes multiple times, and you may reuse edges.git

Example 1:github

Input: [[1,2,3],[0],[0],[0]]
Output: 4
Explanation: One possible path is [1,0,2,0,3]

Example 2:數組

Input: [[1],[0,2,4],[1,3,4],[2],[1,2]]
Output: 4
Explanation: One possible path is [0,1,4,2,3]

Note:編碼

  1. 1 <= graph.length <= 12
  2. 0 <= graph[i].length < graph.length



這道題說是給了一個無向圖,裏面有N個結點,讓咱們找到一條能夠通過全部結點的路徑,該路徑的起點和終點任意選,只要能通過全部結點便可,這裏的每一個結點和邊均可以重複通過,問這樣一條路徑的最短長度是多少,注意這裏的長度不是路徑結點的個數,而是結點中的邊的個數。先來想一下,假如這些結點是一字排開的,則最短通過全部結點的路徑就相似於遍歷鏈表同樣的,但假如這些結點是圍繞着一箇中心結點的話,好比本題中的例子1,則中心結點會被通過屢次,感受不太好整啊。博主以前說過求極值的問題有兩大神器,動態規劃 Dynamic Programming 和廣度優先搜索 Breadth First Search,這裏碰巧兩種方法都能解。先來看看 BFS 的解法吧,這種解法最經典的應用是在迷宮問題中,找到起點和終點之間的最短距離,假如把每一個位置都看做一個狀態的話,BFS 能夠推廣到更通常的狀況。在迷宮中每一步可能會有上下左右四個方向能夠選,每走一步其實能夠看做是一個狀態轉移到另外一個狀態,當到達終點狀態時,就能夠獲得最少步數了。這裏也是相似,首先要定義起始狀態和終止狀態,本題關心的是要通過全部的結點,終止狀態就是通過全部結點,起始狀態就是隻通過了起始結點,那該如何編碼這些狀態呢?最直接的方法就是把通過的結點放到數組或者 HashSet 中,可是這樣的話每次檢驗是否到達終止狀態的時候,都要檢測數組或者 HashSet 中是否包含了全部的結點,這會很費時,由於在 BFS 的每一層遍歷中都會檢測是否到達終止狀態。還有就是每一個狀態是由當前遍歷的結點跟當前結點標號組成的,假如把遍歷過的結點放到數組或集合中,再跟當前結點標號一塊兒組成 pair 對兒放入隊列 queue 中,將會佔用大量的空間。code

基於以上分析,貌似必須想一種更好的方法來編碼遍歷過的結點,這裏用到了位操做 Bit Manipulation 的技巧,對沒使用過的童鞋來講會比較 tricky。對於任意結點i,假如遍歷過了,則將其對應位上變爲1,即 ‘或’ 上 1<<i,這樣每一個結點均可以被分別編碼進對應位,則遍歷過n個結點的十進制數就是 2^n-1 了,只要某個狀態的十進制數等於 2^n-1,則表示到達了終止狀態。另外,因爲最短路徑的起點不定,那麼這裏的 BFS 的起點就應該是全部的結點,將每一個結點都看成起始結點,並將結點編號編碼到十進制數中,和當前位置一塊兒組成 pair 對兒放進隊列中。將n個起點都放入隊列以後,就能夠開始遍歷了,它們都屬於同一層,這裏進行的是 BFS 的層序遍歷的形式。對於每一個取出的元素,首先判斷取出的狀態的 pair 對兒的第一個編碼十進制數是否等於最終結果值 target,是的話直接返回結果 res。而後再根據第二個位置值去 graph 數組中查找全部與其相鄰的結點,對於每一個相鄰的結點 next,因爲在以前的基礎上又加上告終點 next,這也要編碼進去,因此要 ‘或’ 上 1<<next,而後在 visted 集合中查找該新狀態是否存在,不存在的話加入 visited 集合,並把編碼成的十進制數 path 和當前結點編號 next 組成新的 pair 對兒加入隊列進行下次遍歷。每層遍歷結束後記得結果 res 要自增1,while 循環退出後返回 -1,其實根本不會返回 -1,由於題目中是無向連通圖,必定會有通過全部結點的路徑存在,這裏只是怕不寫返回值會報錯而已,參見代碼以下:htm



解法一:blog

class Solution {
public:
    int shortestPathLength(vector<vector<int>>& graph) {
        int n = graph.size(), target = 0, res = 0;
        unordered_set<string> visited;
        queue<pair<int, int>> q;
        for (int i = 0; i < n; ++i) {
            int mask = (1 << i);
            target |= mask;
            visited.insert(to_string(mask) + "-" + to_string(i));
            q.push({mask, i});
        }
        while (!q.empty()) {
            for (int i = q.size(); i > 0; --i) {
                auto cur = q.front(); q.pop();
                if (cur.first == target) return res;
                for (int next : graph[cur.second]) {
                    int path = cur.first | (1 << next);
                    string str = to_string(path) + "-" + to_string(next);
                    if (visited.count(str)) continue;
                    visited.insert(str);
                    q.push({path, next});
                }
            }
            ++res;
        }
        return -1;
    }
};



再來看一種 DP 的解法,這種解法的核心思想跟上面的 BFS 方法很相似,咱們用一個二維的 dp 數組,其中 dp[i][j] 表示的某個狀態時通過的結點編碼成的十進制數i,且當前位置爲結點j時的路徑長度。這樣的話只要當i到達 2^n-1 的時候,此時全部的 dp[2^n-1][j] 中的最小值即爲所求,這種定義狀態的方式能夠說和上面的解法徹底同樣。就像上面解法中將n個結點都看成起始點,並將其狀態存入隊列中的操做同樣,這裏要將全部的 dp[1<<i][i] 初始化爲0,而後遍歷全部可能的十進制編碼值 cur,從0到 2^n-1,新建一個 boolean 型變量 repeat,進行 repeat 爲 true 的循環。在循環中,先將 repeat 賦值爲 false,而後遍歷全部結點,對於遍歷到的結點i,取出其編碼值爲 cur 的 dp 值 dist,而後遍歷與其相連的全部結點,對於每一個遍歷到的結點 next,仍是先用 cur ‘或’ 上 1<<next 獲得新的編碼值,而後進行鬆弛操做 Relaxation,即若 dp[path][next] 值大於 dist+1,則用 dist+1 來更新 dp[path][next]。同時判斷若 path 和 cur 相等,將 repeat 賦值爲 true,從而須要再次進行循環。由於若 path 等於 cur,說明該相鄰結點 next 在以前已經被編碼進 cur 了,但依然能進行鬆弛操做的話,就須要再次遍歷一遍全部結點,以保證在當前編碼 cur 的狀況下,將全部的鬆弛操做都進行完。最後用 dp 數組的最後一行來更新結果 res,其中的最小值即爲所求,參見代碼以下:隊列



解法二:

class Solution {
public:
    int shortestPathLength(vector<vector<int>>& graph) {
        int n = graph.size(), res = n * n;
        vector<vector<int>> dp(1 << n, vector<int>(n, n * n));
        for (int i = 0; i < n; ++i) dp[1 << i][i] = 0;
        for (int cur = 0; cur < (1 << n); ++cur) {
            bool repeat = true;
            while (repeat) {
                repeat = false;
                for (int i = 0; i < n; ++i) {
                    int dist = dp[cur][i];
                    for (int next : graph[i]) {
                        int path = cur | (1 << next);
                        if (dist + 1 < dp[path][next]) {
                            dp[path][next] = dist + 1;
                            if (path == cur) repeat = true;
                        }
                    }
                }
            }
        }
        for (int num : dp.back()) {
            res = min(res, num);
        }
        return res;
    }
};



Github 同步地址:

https://github.com/grandyang/leetcode/issues/847



相似題目:

Shortest Path to Get All Keys



參考資料:

https://leetcode.com/problems/shortest-path-visiting-all-nodes/

https://leetcode.com/problems/shortest-path-visiting-all-nodes/discuss/135712/Java-BFS

https://leetcode.com/problems/shortest-path-visiting-all-nodes/discuss/152679/Short-Java-Solution-BFS-with-a-Set



LeetCode All in One 題目講解彙總(持續更新中...)

相關文章
相關標籤/搜索