10.2 动态规划算法套路及空间优化 —— Climbing Stairs & Unique Paths

这一篇文章从最简单的动态规划题目开始,结合上一节动态规划三要素,以LeetCode两道基础的DP题目阐述DP问题的基本套路解法。


 

70. Climbing Stairs

You are climbing a stair case. It takes n steps to reach to the top.

Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

Note: Given n will be a positive integer.

 

题目解析:

这是一道最基础的动态规划问题,用于我们初步了解。首先我们来回忆DP三大要素,抽烟喝酒烫头,不,是最优子结构,状态转移方程和边界。

  1.  最优子结构,就是分析问题的关键,构建解题框架。之前以斐波那契数列为例,这次我们以这道题目为例。想知道到达n层台阶,有多少种方式,即f(n)是最终想要的答案,抽象为f(i) 即到达任何一层台阶的方式;所谓子结构,即f(i-1),f(i-2),f(i-3),...,f(2),f(1)。很明显,f(i)的方案数目和 f(i-1)等子问题的答案是有关系的。这就是第一步分析问题我们要做的。
  2. 状态转移方程。f(i)和子问题具体是什么关系呢?这一步要定量分析。由于每次只能走一步或两步,一步之前的台阶在i-1层,两步之前在i-2层,那么第i层的方案数目不就是:f(i) = f(i-1) + f(i-2),(还是斐波那契数列)。这一步定量分析问题和子问题之间的数量关系,
  3. 边界。到这儿就很好理解了,可以定义 f(1)=1, f(2)=2.
  4. 其实还有第四步,就是编码啦,能把你的思路写出来才行。

下面我们看代码实现,在dp问题中,对于f(i)我们一般存为数组dp.

class Solution:
    def climbStairs(self, n: int) -> int:
        # 特殊情况
        if n <= 2:
            return n
        # dp数组必备
        dp = [0] * (n+1)
        # 边界
        dp[1] = 1
        dp[2] = 2
        
        for i in range(3, n+1):
            # 状态转移方程
            dp[i] = dp[i-1] + dp[i-2]
            
        return dp[-1]        

 

这个一维的问题就图森破了,下面我们看经典的二维dp问题。说来说去,二维不一定比一维的就难,一维的数组也有很多难题。

62. Unique Paths

A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).

How many possible unique paths are there?

 

题目解析:

我们仿照上题的思路来解析,看三大要素。f(i,j)表示到达i行j列的路径数,f(m,n)即最终结果。i行j列是从左侧或上边走过来的,必然有一定的最优子结构的形式。那么接下来分析状态转移方程。和上一题类似,i行j列的上一步或者在i-1行j列,或者在i行j-1列,只有这两种情况,且每种情况下只有一条路径到i,j,因此直接得到f(i,j) = f(i-1,j) + f(i,j-1)。二维问题的边界也在二维数组的边界,下面我们直接看代码实现:

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # dp数组
        dp = [[0] * n for _ in range(m)]
        # 边界
        for i in range(m):
            dp[i][0] = 1
        for i in range(n):
            dp[0][i] = 1
        
        for i in range(1, m):            
            for j in range(1, n):
                # 状态转移方程
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
        return dp[m-1][n-1] # 或dp[-1][-1]

再额外解释一下边界,沿着第一行或者第一列都是只有一种路径,所以边界的初始值都是1,也可以直接把dp数组初始化为1.


先充分的消化一下上面两道问题的dp套路,从三要素理解解题思路,进而理解dp的核心思想,最优子结构和无后效性。

根据无后效性,下面我们看一下dp算法的优化思路。

以climbing stairs为例,基本思路中我们定义了长度为n的数组用于存储i处的最终解,时间复杂度O(n);但是从关系式dp[i] = dp[i-1] + dp[i-2]可以看出,在i处我们只需要i-1和i-2的值,更早的值我们不需要了,这就体现了无后效性,因此我们只需要三个变量即可解决问题,下面看代码:

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        pre1 = 2
        pre2 = 1
        dp = 0
        for i in range(3, n+1):            
            dp = pre1 + pre2
            pre2, pre1 = pre1, dp
        return dp        

就是这样,将空间复杂度降到了O(1)。我们再看unique paths题目的优化。

对于第i行第j列,我们看关系式 f(i,j) = f(i-1,j) + f(i,j-1),在前面的解法中,我们的空间复杂度是O(m*n),对于每个位置来说,我们需要左边的和上边的,对于每一行来说,我们只需要上一行的结果即可。因此我们可以将空间复杂度降到O(n),结合下面代码来看,我们只定义一个一维dp数组,在每行遍历中,不断更新dp数组的值,不断抛弃上一行的结果,直到最后一行。

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # 定义一维dp数组,同时初始化了第一行的边界值
        dp = [1] * n
        
        for i in range(1, m):            
            for j in range(n):
                # 第一列的边界
                if j == 0:
                    dp[j] = 1
                else:
                    # 相当于原状态转移方程,不断更新dp数组值
                    dp[j] += dp[j-1]
        return dp[-1]

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值