一、動態規劃三大步驟
之前在微信公衆號【帥地玩編程】上看到有人分享,解決動態規劃問題的三大步驟,今天使用這個解題套路,解決LeetCode中不同路徑問題。
這個問題,LeetCode中共有三題,難度從由低到高,分別爲【62題 不同路徑】、【63題 不同路徑2】、【980題 不同路徑3】。
先上解題套路(下面是作者原話):
動態規劃,無非就是利用歷史記錄,來避免我們的重複計算。而這些歷史記錄,我們得需要一些變量來保存,一般是用一維數組或者二維數組來保存。下面我們先來講下做動態規劃題很重要的三個步驟,
第一步驟:定義數組元素的含義,上面說了,我們會用一個數組,來保存歷史數組,假設用一維數組 dp[] 吧。這個時候有一個非常非常重要的點,就是規定你這個數組元素的含義,例如你的 dp[i] 是代表什麼意思?
第二步驟:找出數組元素之間的關係式,我覺得動態規劃,還是有一點類似於我們高中學習時的歸納法的,當我們要計算 dp[n] 時,是可以利用 dp[n-1],dp[n-2]…..dp[1],來推出 dp[n] 的,也就是可以利用歷史數據來推出新的元素值,所以我們要找出數組元素之間的關係式,例如 dp[n] = dp[n-1] + dp[n-2],這個就是他們的關係式了。而這一步,也是最難的一步,後面我會講幾種類型的題來說。
學過動態規劃的可能都經常聽到最優子結構,把大的問題拆分成小的問題,說時候,最開始的時候,我是對最優子結構一夢懵逼的。估計你們也聽多了,所以這一次,我將換一種形式來講,不再是各種子問題,各種最優子結構。所以大佬可別噴我再亂講,因爲我說了,這是我自己平時做題的套路。
第三步驟:找出初始值。學過數學歸納法的都知道,雖然我們知道了數組元素之間的關係式,例如 dp[n] = dp[n-1] + dp[n-2],我們可以通過 dp[n-1] 和 dp[n-2] 來計算 dp[n],但是,我們得知道初始值啊,例如一直推下去的話,會由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我們必須要能夠直接獲得 dp[2] 和 dp[1] 的值,而這,就是所謂的初始值。
不太理解,下面將其應用到題目中就能明白。
二、不同路徑
題目(難度:中等):
一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲“Start” )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲“Finish”)。
問總共有多少條不同的路徑?
步驟一、定義數組元素的含義
由於我們的目的是從左上角到右下角一共有多少種路徑,那我們就定義 dp[i] [j]的含義爲:當機器人從左上角走到(i, j) 這個位置時,一共有 dp[i] [j] 種路徑。那麼,dp[m-1] [n-1] 就是我們要的答案了(網格爲m*n)。
步驟二:找出關係數組元素間的關係式
想象以下,機器人要怎麼樣才能到達 (i, j) 這個位置?由於機器人可以向下走或者向右走,所以有兩種方式到達
一種是從 (i-1, j) 這個位置走一步到達
一種是從(i, j - 1) 這個位置走一步到達
因爲是計算所有可能的步驟,所以是把所有可能走的路徑都加起來,所以關係式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。
步驟三、找出初始值
顯然,當 dp[i] [j] 中,如果 i 或者 j 有一個爲 0,那麼還能使用關係式嗎?答是不能的,因爲這個時候把 i - 1 或者 j - 1,就變成負數了,數組就會出問題了,所以我們的初始值是計算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。這個還是非常容易計算的,相當於計算機圖中的最上面一行和左邊一列。因此初始值如下:
dp[0] [0….n-1] = 1; // 相當於最上面一行,機器人只能一直往左走
dp[0…m-1] [0] = 1; // 相當於最左面一列,機器人只能一直往下走
上代碼
public static int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// 初始值先賦值
for(int i=0; i<m; i++){
dp[i][0] = 1;
}
for(int j=0; j<n; j++){
dp[0][j] = 1;
}
// 通過關係式推導出dp[m-1][n-1]
for(int i=1; i<m; i++){
for(int j=1; j<n; j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
三、不同路徑2
題目(難度:中等):
一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲“Start” )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲“Finish”)。
現在考慮網格中有障礙物。那麼從左上角到右下角將會有多少條不同的路徑?
步驟一、定義數組元素的含義
數組含義仍然跟上一題一樣,當機器人從左上角走到(i, j) 這個位置時,一共有 dp[i] [j] 種路徑。仍然是求dp[m-1] [n-1] 。
步驟二:找出關係數組元素間的關係式
跟上一題相同,機器人要怎麼樣才能到達 (i, j) 這個位置?由於機器人可以向下走或者向右走,所以有兩種方式到達
一種是從 (i-1, j) 這個位置走一步到達
一種是從(i, j - 1) 這個位置走一步到達
因爲是計算所有可能的步驟,所以是把所有可能走的路徑都加起來,所以關係式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。
因爲有障礙了,如果(i,j)這個位置是障礙,那麼不可能到達(i,j)這個位置,那麼dp[i] [j] = 0。
步驟三、找出初始值
我們的初始值還是計算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。相當於計算機圖中的最上面一行和左邊一列。
dp[0] [0….n-1] = 1; // 相當於最上面一行,機器人只能一直往左走
dp[0…m-1] [0] = 1; // 相當於最左面一列,機器人只能一直往下走
但是跟上一題有個不同的地方,如果最上面一行或者最左邊一列存在障礙,假設(0,j)存在障礙,dp[0][j] = 0,很好理解,因爲肯定無法到達這個位置;值得注意,dp[0][j+1....n-1] = 0,因爲(0,j)存在障礙,那就不可能一直往左走了,所以(0,j)右邊都是0。
上代碼
public static int UniquePathsWithObstacles(int[][] obstacleGrid) {
int m= obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
if(obstacleGrid[0][0] == 1){
return 0;
}else {
dp[0][0] = 1;
}
// 初始化,如果第一列存在障礙,第一列後面都是0
for(int i=1; i<m; i++){
if(obstacleGrid[i][0] == 1){
dp[i][0] = 0;
}else{
dp[i][0] = dp[i-1][0];
}
}
// 初始化,如果第一行存在障礙,第一行後面都是0
for(int j=1; j<n; j++){
if(obstacleGrid[0][j] == 1){
dp[0][j] = 0;
}else{
dp[0][j] = dp[0][j-1];
}
}
// 推導出dp[m-1][n-1]
for(int i=1; i<m; i++){
for(int j=1; j<n; j++){
if(obstacleGrid[i][j] == 1){
dp[i][j] = 0;
}else{
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
}
return dp[m-1][n-1];
}
四、不同路徑3
題目(難度:困難):
在二維網格 grid 上,有 4 種類型的方格:
1 表示起始方格。且只有一個起始方格。
2 表示結束方格,且只有一個結束方格。
0 表示我們可以走過的空方格。
-1 表示我們無法跨越的障礙。
返回在四個方向(上、下、左、右)上行走時,從起始方格到結束方格的不同路徑的數目,每一個無障礙方格都要通過一次。
本題不會用動態規劃,實力不允許,找不到數組元素間的關係式,使用遞歸進行解答。
從起點1開始嘗試遍歷每一個 0 方格,並且將走過的方格放入一個map,用於標記走過了。當回溯時,必須將標記的方格從map中移除。
如果到達障礙點-1,則返回,此路不通。
如果到達終點2,並且經過所有無障礙方格(map的size等於所有除-1方格數),則表示map中是一條成功路徑,成功路徑+1
上代碼
package dynamicProgram;
import java.util.LinkedHashMap;
import java.util.Map;
public class uniquePathsIII {
public static void main(String[] args){
int[][] obstacleGrid = new int[][]{{1,0,0,0},{0,0,0,0},{0,0,2,-1}};
System.out.println("成功路徑總數:" + uniquePathsIII(obstacleGrid));
}
public static int uniquePathsIII(int[][] grid) {
int m= grid.length;
int n = grid[0].length;
int starti=0;
int startj=0;
int step= 0;
for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
if(grid[i][j] == 0 || grid[i][j] == 1 || grid[i][j] == 2){
step++;
}
if(grid[i][j] == 1){
starti = i;
startj = j;
}
}
}
Map<String, String> map = new LinkedHashMap<>();
map.put("(" + starti + ", " + startj + ")", "0");
int ret = 0;
if(starti + 1 < grid.length){
ret += access(starti+1, startj, grid, step, map);
}
if(starti - 1 >= 0){
ret += access(starti - 1, startj, grid, step, map);
}
if(startj + 1 < grid[0].length){
ret += access(starti, startj+1, grid, step, map);
}
if(startj - 1 >= 0){
ret += access(starti, startj-1, grid, step, map);
}
return ret;
}
public static int access(int x, int y, int[][] grid, int step, Map<String, String> map) {
// 標記當前方格走過
map.put("(" + x + ", " + y + ")", "0");
// 如果到達終點,並且走遍所有無障礙方格,則爲一條成功路徑
if (grid[x][y] == 2 && step == map.size()) {
// 打印當前成功路徑
System.out.println("成功路徑;");
for(Map.Entry<String, String> mapEntry : map.entrySet()){
System.out.print(mapEntry.getKey() + " - ");
}
System.out.println();
// 因爲map相當於全局變量,一定移除走過的方格
map.remove("(" + x + ", " + y + ")");
return 1;
}
if (grid[x][y] == -1) {
// 因爲map相當於全局變量,一定移除走過的方格
map.remove("(" + x + ", " + y + ")");
return 0;
}
int ret = 0;
// 上下左右四個方向,並且是沒走過的方格,都走一遍
if (x + 1 < grid.length && (map.get("(" + (x + 1) + ", " + y + ")") == null)) {
ret += access(x + 1, y, grid, step, map);
}
if (y + 1 < grid[0].length && (map.get("(" + x + ", " + (y + 1) + ")") == null)) {
ret += access(x, y + 1, grid, step, map);
}
if (x - 1 >= 0 && (map.get("(" + (x - 1) + ", " + y + ")") == null)) {
ret += access(x - 1, y, grid, step, map);
}
if (y - 1 >= 0 && (map.get("(" + x + ", " + (y - 1) + ")") == null)) {
ret += access(x, y - 1, grid, step, map);
}
// 因爲map相當於全局變量,一定移除走過的方格
map.remove("(" + x + ", " + y + ")");
return ret;
}
}
我打印了成功路徑,看看結果
上面代碼從寫法上,比較容易理解,再上一版,從代碼寫法上簡潔點的,邏輯相同。
上代碼:
package dynamicProgram;
public class UniquePathsIIIConcise {
int ans;
int[][] grid;
int tr, tc;
// 表示四個方向,不用四個if語句判斷了
int[] dr = new int[]{0, -1, 0, 1};
int[] dc = new int[]{1, 0, -1, 0};
int R, C;
public static void main(String[] args){
int[][] obstacleGrid = new int[][]{{1,0,0,0},{0,0,0,0},{0,0,2,-1}};
UniquePathsIIIConcise uniquePathsIIIConcise = new UniquePathsIIIConcise();
System.out.println("成功路徑總數:" + uniquePathsIIIConcise.uniquePathsIII(obstacleGrid));
}
public int uniquePathsIII(int[][] grid) {
this.grid = grid;
R = grid.length;
C = grid[0].length;
int todo = 0;
int sr = 0, sc = 0;
for (int r = 0; r < R; ++r)
for (int c = 0; c < C; ++c) {
if (grid[r][c] != -1) {
todo++;
}
if (grid[r][c] == 1) {
sr = r;
sc = c;
} else if (grid[r][c] == 2) {
tr = r;
tc = c;
}
}
ans = 0;
dfs(sr, sc, todo);
return ans;
}
public void dfs(int r, int c, int todo) {
todo--;
if (todo < 0) return;
// 到達終點,並且經過所有無障礙方格
if (r == tr && c == tc) {
if (todo == 0) ans++;
return;
}
grid[r][c] = 3;
for (int k = 0; k < 4; ++k) {
int nr = r + dr[k];
int nc = c + dc[k];
if (0 <= nr && nr < R && 0 <= nc && nc < C) {
// 沒經過,並且不是障礙方格-1
if (grid[nr][nc] % 2 == 0)
dfs(nr, nc, todo);
}
}
grid[r][c] = 0;
}
}