算法学习|Day39 动态规划part02|Leetcode 62.不同路径;63.不同路径II

1. 62.不同路径

1.1 思路

机器人从(0 , 0) 位置出发,到*(m - 1, n - 1)*终点。

按照动规五部曲来分析

  1. 确定dp数组以及下标的含义
    dp[i] [j] :从(0 ,0)出发,到(i, j) 有dp[i] [j]条不同的路径;

    ps: 数组不是dp[m+1][n+1],这里不需要

  2. 确定递推公式

    想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j]dp[i] [j - 1]

    回顾一下 dp[i - 1] [j] 表示:是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理;

    所以很自然:dp[i][j] = dp[i - 1] [j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来;

  3. dp数组的初始化
    dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理 (题目说了只能向左或向右出发)

    所以初始化代码为:

    for (int i = 0; i < m; i++) dp[i][0] = 1;
    for (int j = 0; j < n; j++) dp[0][j] = 1;
    
  4. 确定遍历顺序
    这里要看一下递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1]dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了;

    这样就可以保证推导dp[i][j]的时候,dp[i - 1][j]dp[i][j - 1]一定是有数值的;

  5. 举例推导dp数组

1.2 代码实现

class Solution {
    public int uniquePaths(int m, int n) {
        //确定dp含义 dp[i][j]是移动到[i][j]位置有几种路径
        //因为dp[i][0]和dp[0][j]算是数组中的了,所以设置的数组长度是[m][n]
        int[][] dp=new  int[m][n];
        //初始化
        //每一列值都是1
        for (int i = 0; i < m; i++) {
            dp[i][0]=1;
        }
        for (int i = 0; i < n; i++) {
            dp[0][i]=1;
        }
        //遍历顺序:从上往下,从左往右
        //0行和0列已经遍历了,所以从1行和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];
    }
 }
  • 时间复杂度:O(m*n),网格的大小为m x n ;遍历需要从左到右,从上到下,把所有格子都遍历一次,每次操作一个加法运算;
  • 空间复杂度:O(m*n),dp数组是一个二维数组,大小和网格一样也为m x n;

2. 63.不同路径II

2.1 思路

本题相对于62 不同路径 就是多了障碍物。有障碍的话,标记对应的dp数组保持初始值0

动态五部曲

  1. 定义dp数组以及下标含义

    dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径

  2. 确定递推公式

    递推公式和 62.不同路径 一样,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],

    但需要注意一点:因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0

    if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j]
        dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
    }
    
  3. 初始化数组

    在 62.不同路径 我们把第0行和第0列全部初始化为1;因为从(0, 0)的位置到(i, 0)的路径只有一条,所以dp[i][0]一定为1,dp[0][j]也同理。

    需要注意的是:

    • 在本题中,如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0;下标(0, j)的初始化情况同理;

    • 代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理;

  4. 确定遍历顺序

    从上到下,从左到右

  5. 举例倒推dp数组

2.2 代码实现

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;//网格长
        int n = obstacleGrid[0].length;//网格宽度
        //定义数组
        int[][] dp = new int[m][n];//dp表示走到(i,j)位置共有的路径

        //起点或终点出现障碍,返回0;
        if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) {
            return 0;
        }

        //初始化数组
        //初始化行
        for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
            dp[i][0] = 1;//注意dp含义:是到(i,0)位置共有多少条路径
        }
        //初始化列
        for (int i = 0; i < n && obstacleGrid[0][i]==0; i++) {
            dp[0][i] = 1;
        }
        //遍历数组  从上到下,从左右到右
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                //注意此处递归方法
                dp[i][j] = (obstacleGrid[i][j] == 0) ? dp[i - 1][j] + dp[i][j - 1] : 0;
            }
        }
        return dp[m - 1][n - 1];
    }
}
  • 时间复杂度:O(m*n)

    网格的大小为m x n ;for循环需要从左到右,从上到下,把所有格子都遍历一次,每次操作一个加法运算;

  • 空间复杂度:O(m*n)

    dp数组是一个二维数组,大小和网格一样也为m x n;

2.3 方法优化 ——一维数组

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[] dp = new int[n];

        //若有障碍物,则停止遍历,后面都是空,因为有障碍物的话,后面就到不了,直接停止遍历
        for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
            dp[j] = 1;
        }

        for (int i = 1; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (obstacleGrid[i][j] == 1) {
                    dp[j] = 0;
                } else if (j != 0) {
                    dp[j] += dp[j - 1];//一个位置的路径数等于从上方和左侧到达该位置的不同路径数之和。
                }
            }
        }
        return dp[n - 1];
    }
}
  • 时间复杂度:O(m*n)

  • 空间复杂度:O(n)。因为只用开辟了一个一维 dp 数组,这种滚动数组的优化方式常用于解决动态规划中的空间问题

这段代码主要的思路与之前的代码实现略微不同,不需要使用二维的 dp 数组来计算中间结果,而是采用==滚动数组==的方式来优化空间

具体来说:

  • 代码定义一个一维的 dp 数组

    • 其中 dp[j] 表示到达(i,j)的不同路径数目
    • dp[j-1] 表示当前位置左侧的位置上的路径数
    • dp[j] 表示当前位置上方位置的路径数

    因此递推公式是:dp[j] += dp[j - 1]

  • 对于第一行和第一列的位置,和之前的代码实现一样,到达这些位置的路径数只有一种

  • 循环处理非首行和非首列的位置

    • 若当前位置为障碍,则该位置的路径数必定为 0,
    • 否则,该位置的路径数=它左侧位置的路径数+上方位置的路径数之和
  • 最后,返回 dp 数组的最后一项即可得到从起始节点到终止节点的路径数。

2.4 总结及思考

  1. 在动态规划问题中,可以采用滚动数组的优化方式于解决动态规划中的空间问题。
  2. 遍历总是从数组初始化的下一行,下一列开始的,所以i,j一般都是1开始
  3. 此题跟62.不同路径相似,但也有很多细节,比如初始化的部分,很容易忽略了障碍之后应该都是0的情况

你可能感兴趣的:(算法笔记,算法,学习,动态规划,leetcode,数据结构)