代码随想录算法训练营打卡Day39 | LeetCode62 不同路径、LeetCode63 不同路径II

摘要

  • 从到达当前状态的有几种可能来思考,是一种确定dp数组及数组下标含义的方式

  • 滚动数组是一种能够在动态规划中降低空间复杂度的方法,当我们在使用递推方程计算下一步状态的时候,可能只需要用到之前几个状态的数据而不是全部的数据。在这种情况下,我们就可以对推导下一步状态没用的数据进行覆盖。

LeetCode62 不同路径

62. 不同路径 - 力扣(Leetcode)

  • 确定dp数组及数组下标的含义:dp[i][j]为到达第i行第j列的可能路径数。
  • 确定递推方程,先考虑简单的子问题:
    1. 只有“向右走”可以到达第一行(i=0)的格子,因为第一行的上面没有格子,没有“向下走”到达第一行的格子的可能。
    2. 只有“向下走”可以到达第一列(j=0)的格子,因为第一列的左边没有格子,没有“向右走”到达第一列的格子的可能。
    3. 对于第一行或第一列以外的格子,到达第i行第j列的格子,只有两种可能,一是从第i-1行第j列的格子向下走一格,二是从第i行第j-1列的格子向右走一格。

dp[i][j]= \begin{cases}1, & i=0 \\ 1, & j=0 \\ dp[i-1][j]+dp[i][j-1], & i>0 \wedge j>0 \\ \end{cases}

  • 由递推方程,可以得到dp数组的初始状态,即i=0的格子和j=0的格子的dp值都初始化为1

  • 确定遍历方法,在本题中,“向下走”和“向右走”都是更趋近终点的走法,所以先向下还是先向右应该是一样的,本题先遍历i还是先遍历j只是先更新行还是先更新列的区别。

题解代码如下

class Solution {
public:
    int uniquePaths(int m, int n) {
        // dp数组初始化
        vector<vector<int>> dp(m, vector<int>(n));
        for (int i = 0; i < m; i++) dp[i][0] = 1;
        for (int j = 0; j < n; j++) dp[0][j] = 1;
        
        // 构造dp数组
        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];
            }
        }
        
        // 返回结果,注意是左闭右开区间,终点对应的下标为(m-1, n-1)
        return dp[m - 1][n - 1];
    }
};
  • 本题可以用滚动数组的思想进行优化
    • 滚动数组是一种能够在动态规划降低空间复杂度的方法,在一些特殊情况下,二维动态规划方程甚至可以直接降阶到一维,是一种极为巧妙的思想。
    • 如果把dp数组看作一个表格,滚动数组可以理解为dp数组中的滑动窗口。
    • 当我们在使用递推方程计算下一步状态的时候,可能只需要用到之前几个状态的数据而不是全部的数据。在这种情况下,我们就可以对推导下一步状态没用的数据进行覆盖
  • 回顾我们之前推出的递推公式dp[i][j]=dp[i-1][j]+dp[i][j-1], i>0 \and j>0。显然,对dp[i][j]的计算并不需要知道之前所有格子各自的路径数,只需要知道dp[i][j-1]dp[i-1][j]
  • 那如何体现在代码上呢?我们先来看原来的二维的dp数组是如何计算出答案的:
二维dp数组的构造过程
  1. 先初始化
  1. 然后,从dp[1][1]开始,逐行(或逐列)计算dp[i][j]的值,由dp[i-1][j]dp[i][j-1]相加得到。
  1. 最终得到终点的可能路径数,此时dp数组如下
滚动数组dp的构造过程
  • 那么滚动数组是如何只用一维数组来计算出到达终点的可能路径数的呢?在使用二维的dp数组时,我们依然是逐行(或逐列)构造的。那么是否可以只用一行来保存计算下一状态所需的信息呢?在本题当然是可行的,我是用滑动窗口的方式理解的。

  • 还是在之前那张代表二维dp数组的表格上看,青蓝色的格子代表当前dp数组保存的值。

  1. 初始化dp数组
  1. 接下来我们要逐行向下滑动这个窗口,怎么滑动呢?我先滑动第一个格子,第2行第1列的格子当然只能由起点向下走一格到达,所以值为1,这是自然的,为了“推出”这个值,dp[0]应该初始化为1

    • 这与二维dp数组dp[0][0]的初始化是不同的,其实可以看出,除了m=1; n=1的情况,二维dp数组中的dp[0][0]初始化成什么值都不会影响得到正确的结果,因为当m>1; n>1时,根本没有哪一个格子的计算会用到dp[0][0]

    • 可以拿以下代码验证上述说法

    • class Solution {
      public:
          int uniquePaths(int m, int n) {
              vector<vector<int>> dp(m, vector<int>(n));
              for (int i = 0; i < m; i++) dp[i][0] = 1;
              for (int j = 0; j < n; j++) dp[0][j] = 1;
      
              if (m != 1 || n != 1) {
                  srand(time(0));
                  dp[0][0] = rand();
              }
      
              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];
          }
      };
      
  1. 回到正题,明确dp[0]要初始化成1,我们由dp[0]推出了“新的”dp[0],表现为滑动了窗口的第一个格子,将窗口的第一个格子向下滑动了一格。接下来应该滑动第二个格子,其实到这里就可以看出所谓“滚动数组”是怎么节省空间的了,”新的“dp[0]正好可以和“旧的”dp[1]加算出“新的”dp[1]
  1. 这之后的dp[i]当然也是同理,所谓“滚动数组”,就是用“新的”dp[i-1]和“旧的”dp[i]计算出“新的”dp[i],然后i++重复这一操作。滑动窗口并不是整行整行的滑动的,而是一个格子一个格子轮流向下滑动的。如果把[0-6]画成一个轮子:轮子底部滚动到哪个下标,就更新哪个下标,这或许是一种形象地理解滚动数组的方法。下图是滚动到1
  1. 然后把”轮子”滚完一圈到6,就将“滑动窗口”滑动了一行
  1. 再重复“轮子”的滚动,“轮子”滚动到0,之后的过程就不赘述啦
  • 根据“滚动数组”,我们可以这样定义dp数组:从第一行(第一行下标为0)开始逐行向下走,dp[j]为到达当前行第j列的格子的可能路径,dp[j]的更新次数即为对应的格子所在的行的下标(下标从0开始)。

据此,不难写出如下题解代码

class Solution {
public:
    int uniquePaths(int m, int n) {
        // dp数组初始化
        vector<int> dp(n, 1);
        
        // 注意滑动窗口初始时就在第一行(下标为0的行)了
        // 所以第一次更新 dp 数组对应的应该是第二行(下标为1的行),i初始化为1
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                // dp[0]是一直等于1的,不用显式的更新,从dp[1]开始更新即可
                dp[j] += dp[j - 1];
            }
        }
        //  最终 dp 数组的最后一个值即为答案
        return dp[n - 1];
    }
};

LeetCode63 不同路径II

63. 不同路径 II - 力扣(Leetcode)

  • 这道题目和上一题是连着的,只是在dp数组初始化和构造的过程中加上了条件判断,对应题目提到的障碍物。还是先从直观且容易理解的二维dp数组开始

  • 确定dp数组及数组下标的含义:dp[i][j]为到达第i行第j列的可能路径数。

  • 确定递推方程,先考虑简单的子问题:

    1. 只有“向右走”可以到达第一行(i=0)的格子,因为第一行的上面没有格子,没有“向下走”到达第一行的格子的可能。如果第一行的某个格子出现障碍物,那么这个格子及其右边的格子都是不可到达的。设第一行中第oj列的格字出现障碍物,只有列下标小于最小的oj的格子是可以到达的。
    2. 只有“向下走”可以到达第一列(j=0)的格子,因为第一列的左边没有格子,没有“向右走”到达第一列的格子的可能。如果第一列的某个格子出现障碍物,那么这个格子及其下面的格子都是不可到达的。设第一列中第oi列的格字出现障碍物,只有行下标小于最小的oi的格子是可以到达的。
    3. 对于第一行或第一列以外的格子,到达第i行第j列的格子,只有两种可能,一是从第i-1行第j列的格子向下走一格,二是从第i行第j-1列的格子向右走一格。
    4. 如果第i行第j列的格子有障碍物,说明这个格子是不可到达,到达该格子的可能路径数是0
    • 递推公式如下,偷懒就不用数学语言描述第一行或第一列不可到达的情况了

    dp[i][j]= \begin{cases} 1, & (i<min(oi) \wedge obstacleGrid[oi][0]=1) \vee i < m \\ 1, & (j<min(oj) \wedge obstacleGrid[0][oj]=1) \vee j < n \\ dp[i-1][j]+dp[i][j-1], & i>0 \wedge j>0 \wedge obstacleGrid[i][j]=0 \\ 0, & obstacleGrid[i][j]=1 \vee 第一行或第一列不可到达的情况 \end{cases}

    • 也可以将obstacleGrid[i][j]乘进表达式中,来简化递推公式的条件判断,这在代码实现中也可以起到简化的作用,不用写if语句即可完成判断。

    dp[i][j]= \begin{cases} !obstacleGrid[i][j], & i<min(oi) \\ !obstacleGrid[i][j], & j<min(oj) \\ !obstacleGrid[i][j]*(dp[i-1][j]+dp[i][j-1]), & i>0 \wedge j>0 \\ 0, & 第一行或第一列不可到达的情况 \end{cases}

  • 根据递推公式来初始化dp数组,在这里可以先做一次判断,如果起点或者终点有障碍物,就不需要计算了,起点或终点有障碍物时的可能路径数都是0

  • 遍历方法也和上题没有什么区别,先行后列或先列后行都可以

题解代码如下

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        vector<vector<int>> dp(m, vector<int>(n));
        if (obstacleGrid[0][0] || obstacleGrid[m - 1][n - 1]) return 0;
        for (int j = 0; j < n && !obstacleGrid[0][j]; j++) dp[0][j] = 1;
        for (int i = 0; i < m && !obstacleGrid[i][0]; i++) dp[i][0] = 1;

        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = !obstacleGrid[i][j] * (dp[i - 1][j] + dp[i][j - 1]);
            }
        }

        return dp[m - 1][n - 1];
    }
};
  • 这道题目也可以用一维dp数组来解决,但要注意递推公式到实现代码的变化。改变dp数组的定义,一般都会引起递推公式的变化。
  • 一维dp数组的定义:从第一行(第一行下标为0)开始逐行向下走,dp[j]为到达当前行第j列的格子的可能路径,dp[j]的更新次数即为对应的格子所在的行的下标(下标从0开始),如果对应的格子有障碍物,说明这个格子无法到达,可能路径数为0
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        if (obstacleGrid[0][0] || obstacleGrid[m - 1][n - 1]) return 0;
        vector<int> dp(n);
        // 根据第1行初始化滚动数组
        for (int j = 0; j < n && !obstacleGrid[0][j]; j++) {
            dp[j] = 1;
        }

        for (int i = 1; i < m; i++) {// 从第2行开始初始化滚动数组
            for (int j = 0; j < n; j++) {
                //由于第1列也可能有障碍物,dp[0]是需要我们来控制如何更新的
                if (obstacleGrid[i][j])
                    dp[j] = 0;
                else if (j > 0)// 可以到达且不为第1列的格子
                    dp[j] = dp[j] + dp[j-1];
            }
        }

        return dp.back();
    }
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容