想獲得更好的排版,請移步個人博客: https://pushy.site
無障礙物
題目(原題見 LeetCode - 62. 不同路徑):一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記為“Start” )。機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為“Finish”)。問總共有多少條不同的路徑?
示例 1:
輸入: m = 3, n = 2
輸出: 3
解釋:
從左上角開始,總共有 3 條路徑可以到達右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
通過觀察我們我們得出以下兩個特點:
- 機器人所在的正東方向和正南方向,只能有一條路徑可以走,即直走。
- 到某個點的路徑 = 到該點上一個單位的點路徑 + 到該點左一個單位的點路徑。
為了驗證第二個特點,我們假設網格是 2 * 2。如下圖所示,從 A 到達 D 點的路徑總和就等於 A 點到達 B 點的路徑 + 到達 C 點的路徑總和。
這樣,我們不難寫出遞歸代碼:
public int solution(int m, int n) {
if (m <= 0 || n <= 0)
return 0;
else if (m == 1 || n == 1)
return 1;
int result = 0;
result += rec(m - 1, n);
result += rec(m, n - 1);
return result;
}
遞歸代碼雖然簡單,但是和斐波那契數列遞歸實現一樣,會出現重復子樹的情況。例如求 4 * 3 的路徑時,就出現了遞歸計算重復子樹的情況:
和斐波那契一樣,可以用動態規划來優化復雜度,不難寫出狀態轉移方程為:f(i, j) = f(i - 1)(j) + f(i)(j - 1)
public int solution(int m, int n) {
int[][] dp = new int[n][m];
for (int i = 0; i < n; i++) { // 第一列
dp[i][0] = 1;
}
for (int i = 0; i < m; i++) { // 第一行
dp[0][i] = 1;
}
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[n - 1][m - 1];
}
有障礙物
現在,保持和無障礙物的題意,在網格中設置了障礙物(機器人無法越過障礙物),求到終點的路徑總和。原題見 LeetCode - 63. 不同路徑 II
雖然增加了障礙物,但是前面總結的兩個特點還是適用的。但是,需要做一些改變:
- 對於正東和正南的方向路徑,一旦遇到障礙物(1),代表無法通過,則后面的路徑全是 0;
- 第二個特點僅使用於該點不是障礙物;如果為障礙物,則直接將該點路徑設為 0,代表從該點無法通過。
LeetCode 官方圖解如下:
public int uniquePathsWithObstacles(int[][] matrix) {
if (matrix == null) return 0;
int n = matrix.length;
int m = matrix[0].length;
int[][] dp = new int[n][m];
dp[0][0] = matrix[0][0] == 0 ? 1 : 0;
if (dp[0][0] == 0) {
return 0;
}
// 第一列
for (int i = 1; i < n; i++) {
if (matrix[i][0] != 1) {
dp[i][0] = dp[i - 1][0];
}
}
// 第一行
for (int j = 1; j < m; j++) {
if (matrix[0][j] != 1) {
dp[0][j] = dp[0][j - 1];
}
}
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
if (matrix[i][j] != 1) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[n - 1][m - 1];
}