動態規劃(十八)可行路徑數
問題描述
本問題對應 LeetCode 1575. 統計所有可行路徑](https://leetcode-cn.com/problems/count-all-possible-routes/)。
具體描述如下:給定一個互不相同的整數陣列 locations
,其中 locations[i]
表示第 i
個 城市的位置,同時給定三個引數 start
、finish
、fuel
分別表示出發城市、目的城市和初始的汽油量。在每一步中,可以任意選擇一個一個城市 j
,從上一座城市 i
移動到城市 j
需要消耗的汽油量為 |locations[j] - locations[i]|
。|x|
表示 x
的絕對值。現在需要編寫一個函式,求得從 start
finish
之間滿足條件的可能行駛方案的總數。
前提條件:
- 可以經過一個城市多次,包括
start
和finish
,但是不能每次必須在城市之間進行移動 - 必須確保在城市之間移動的汽油量
fuel
不能是負的 - 由於答案可能會很大,因此需要對最終的結果對
1e9 + 7
進行取餘操作
解決思路
對於路徑查詢的問題,首先會想到使用 dfs
的方式遍歷所有的可能路徑,找到符合條件的路徑進行統計即可。
-
一般
DFS
按照一般的
DFS
的方法進行處理即可,但是需要注意以下幾點:- 由於可以在起始位置和結束位置來回,因此目的城市的位置不是
DFS
的結束條件 - 由於需要保證有充足的汽油量在城市之間進行走動,因此汽油量就是
DFS
- 每當有一條路徑能夠到達目的城市時,說明至少存在這麼一條路徑
- 由於可以在起始位置和結束位置來回,因此目的城市的位置不是
-
帶記憶化的
DFS
一般的
DFS
解法在這個問題中會超時,這是因為在DFS
的過程中大量重複計算了之前的已經計算過的情況,由於重複計算的原因導致上文提到的一般的DFS
解決思路的時間複雜度是指數級別的。對於這種由於重複計算導致高額的時間複雜度的情況,一般的解決方案都是使用動態規劃的方式記錄之前的計算結果,這樣可以可以有效降低演算法的時間複雜度。
定義二維陣列
\[cost_{pos,i} = |locations[pos] - locations[i]| \]dp[pos][rest]
表示當前的城市位置為pos
、可用汽油量為rest
的條件下,可以到達目的城市的路徑總數。記表示從
pos
i
需要消耗的汽油量,那麼dp[pos][i]
的狀態轉換函式如下所示: \[dp[pos][rest] = \sum_{i=0}^{n-1} dp[i][rest -cost_{pos,i}] (其中 rest >= cost_{pos,i}) \]邊界情況:噹噹前所處的城市的位置為
finish
時,此時至少存在一種可能的路徑,因此對dp[finish][rest]
需要額外加一進一步的優化:(原題並沒有描述這一特徵,但是在官方的題解中介紹到了)在兩個城市之間穿梭,在最短的距離中的耗油量是最小的,因此在兩個城市之間可能的路徑數在這種情況下的數量是最多的(多餘的
fuel
可以進行更多的路徑移動),因此如果在搜尋過程中發現有耗油量大於這個值的,那麼就可以直接忽略掉,即dp[pos][rest] = 0
。
實現
-
一般
DFS
class Solution { private final static int mod = (int)(1e9 + 7); private int target; // 目標城市位置 public int countRoutes(int[] locations, int start, int finish, int fuel) { target = finish; return dfs(locations, start, fuel); } /** * @param location : 城市的位置資訊列表 * @param cur : 當前所處的城市位置 * @param curFuel:當前行駛過程可用的汽油量 */ private int dfs(int[] locations, int cur, int curFuel) { int sum = 0, take = 0; if (cur == target) sum = 1; // 這裡是邊界情況,處於目的城市時至少存在一條可能的路徑 for (int i = 0; i < locations.length; ++i) { if (i == cur) continue; take = Math.abs(locations[cur] - locations[i]); // 從當前城市 cur 到城市 i 需要花費的汽油量 if (take > curFuel) continue; // 要保證能夠從一個城市到另一個城市 sum += dfs(locations, i, curFuel - take); // 遞迴搜尋即可 sum %= mod; } return sum; } }
複雜度分析,由於對每個位置都需要進行
DFS
搜尋,因此時間複雜度為 $ O(n^fuel) $ (其中,n 表示城市列表的長度,fuel 表示初始的可用汽油量) -
帶記憶化的
DFS
class Solution { private static final int mod = (int)(1e9 + 7); int[][] dp; int n; public int countRoutes(int[] locations, int start, int finish, int fuel) { n = locations.length; dp = new int[n][fuel + 1]; for (int i = 0; i < n; ++i) Arrays.fill(dp[i], -1); // 將它填充為 -1,這是為了區分可能路徑數為 0 和已經訪問過這兩種情況 return dfs(locations, start, finish, fuel); } private int dfs(int[] locations, int pos, int finish, int rest) { if (dp[pos][rest] != -1) // 不為 -1 表示已經被訪問過了,直接拿取結果即可 return dp[pos][rest]; dp[pos][rest] = 0; // 標記為已經訪問過這個情況 if (Math.abs(locations[pos] - locations[finish]) > rest) // 耗油量最小的情況是可能路徑數最大的 return 0; for (int i = 0; i < n; ++i) { if (pos == i) continue; // 必須移動到別的城市 int cost = Math.abs(locations[pos] - locations[i]); if (cost > rest) continue; dp[pos][rest] += dfs(locations, i, finish, rest - cost); dp[pos][rest] %= mod; } // 注意這裡的邊界情況 if (pos == finish) { dp[pos][rest] += 1; dp[pos][rest] %= mod; } return dp[pos][rest]; } }
複雜度分析:相比較一般的
DFS
方式的搜尋,通過動態規劃的方式來記錄之前的訪問情況,大幅度降低了計算的時間複雜度,最終時間複雜度為 \(O(fuel*n^2)\)