动态规划-62.不同路径

112 阅读2分钟

4月日新计划更文活动 第1天

前言

动态规划专题,从简到难通关动态规划。

每日一题

今天的题目是 62. 不同路径,难度为中等

一个机器人位于一个 m x n **网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

 

示例 1:

输入: m = 3, n = 7
输出: 28

示例 2:

输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

输入: m = 7, n = 3
输出: 28

示例 4:

输入: m = 3, n = 3
输出: 6

 

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109

题解

由于机器人每次只能向右或者向下,所以走过的路径是可以用一颗二叉树来表示的,比方说起始位置为 [1,1] 那么下一步移动的位置一定是 [1,2] 或者 [2, 1] 只有这两种可能,

image.png

当然也可以反向思考,终点一定是题目给的 [m, n] 不存在不能够到达的情况,那么可以从 [m, n] 一步步退回起点

image.png

深度搜索

既然我们能够把路径转化为一颗树,那么只需要对这棵树进行深度遍历,找到所以到达叶子节点的方式,就能够得到题目需要的答案,我这里是使用的第二种图,从终点退回来的树图。

function uniquePaths(m, n) {
    const dfs = (x,y) => {
        if(x<1||y<1) {
            return 0
        }
        if(x==1 && y==1) {
            return 1
        }
        return dfs(x-1, y) + dfs(x, y-1)
    }
    return dfs(m,n)
};

当然需要判断一下,退后不能够退出整个图,也就是 m, n 都不能够小于1

就拿题目这张图举例, m = 7 , n = 3 那么当向上退了两次以后 m = 7 , n = 1 那么就不能够再往上退后了,也就是这个树已经算是到头了,后面只能够 向左后退,这就是需要判断的边界情况

但是当我们拿着这个递归代码去提交,就会发现超时了

image.png

因为递归一个二叉树,他的时间复杂度是 O(2^n) 级别的,是属于指数增长的,所以当 m, n 都比较大的时候,就会导致超时。

看图能够发现,其实机器人走过的很多路都是重复的,比方说 从 [m, n] 到达 [m-1, n] 或者 [m, n-1] 之后两个点都可能去到 [m-1, n-1] 这就会导致重复计算

所以我们还是需要动态规划的思路,利用 dp 数组来保存每一个点的状态

动态规划

利用动态规划的五部曲来操作

  1. 确定 dp 数组以及下标的含义,根据我们上面的分析,由于同一个点可能有不同的来源,所以我们要记录的是每一个点位的不同路径,dp[x][y] 代表的是 从 [1,1] 出发,到达[x, y] 有几条不同的路径。

  2. 确定递归公式,想要到达最后的 [m,n] 就只能从 [m-1,n] 或者 [m,n-1] 这两个位置前进,再根据dp数组的含义,dp[m,n] 代表从 [1,1] 出发,到达[m,n] 有几条不同的路径,那么 dp[m,n] 就可以看成 dp[m-1,n] + dp[m,n-1] 。代表到达[m,n] 的方式为 到达[m-1,n]的方式在加上 到达[m,n-1] 的方式的和。

  3. dp数组初始化,首先为了避免在遍历的过程当中下标不存在,我们可以先初始化一个二维数组,大小为 [m][n] 并且将 [1,1] 的位置初始化为 1

  4. 确定遍历顺序,这点就根据上面的说明,可以选择两种遍历方式,一种是从 [1,1] 前进,一种是从 [m,n] 后退,最后结果都是相同的,我这里使用的是 从 [m,n] 后退的方式。

  5. 推导dp数组,以 m = 7 , n = 3 为例

1111111
1234567
13610152128

根据上面深度搜索的代码利用dp数组进行优化得到

function uniquePaths(m, n) {
    let dp = new Array(m).fill(0).map(e=>new Array(n).fill(0))
    console.log(dp);
    const dfs = (x,y) => {
        if(x<1||y<1) {
            return 0
        }
        if(x==1 && y==1) {
            return 1
        }
        if(dp[x-1][y-1]) {
            return dp[x-1][y-1]
        }else {
            let t = dfs(x-1, y) + dfs(x, y-1)
            dp[x-1][y-1] = t
            return t
        }
    }
    return dfs(m,n)
};

时间复杂度降低到了 O(mn), 空间复杂的 为 O(mn), 其中 m ,n 为 题目 中 m,n 的大小

滚动数组

分析上面 7,3 的这种情况的 dp 数组,我们会发现一个规律

1111111
1234567
13610152128

dp [i,j] 的位置等于 dp[i-1, j] 加上 dp [i, j-1] 并且我们最后要的值一定是 dp[m,n] ,所以其实二位数组显得有些浪费,我们可以只建立一个一维数组,比方说 dp[] 然后初始化长度 等于m ,第一行 一定都是等于 1 的,因为只有一种移动方式,然后向下进行滚动,第二行每个位置的值等于 当前的值加上上一行后一个的值,比方说 2 等于 1 + 1,10 等于 6 + 4,。

那么我们就可以写出这个循环

let dp = new Array(n).fill(1)
  
for(let j = 1; j < m ; j ++) {
    for(let i = 1; i < n; i ++) {
        dp[i] = dp[i] + dp[i-1]
    }
}

// 当 m = 7, n = 3 dp = [1, 7, 28]

时间复杂度没变,空间复杂度优化到了 O(n) 只需要一个一维数组
image.png