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



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

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

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.

Example 1:

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 中,將會占用大量的空間。

基於以上分析,貌似必須想一種更好的方法來編碼遍歷過的結點,這里用到了位操作 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,因為題目中是無向連通圖,一定會有經過所有結點的路徑存在,這里只是怕不寫返回值會報錯而已,參見代碼如下:


解法一:
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< 解法二:
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 題目講解匯總(持續更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM