【LeetCode 不同路徑123】使用動態規劃三步驟解決

一、動態規劃三大步驟

之前在微信公衆號【帥地玩編程】上看到有人分享,解決動態規劃問題的三大步驟,今天使用這個解題套路,解決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;
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章