【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;
    }
}

 

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