这一篇文章从最简单的动态规划题目开始,结合上一节动态规划三要素,以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三大要素,抽烟喝酒烫头,不,是最优子结构,状态转移方程和边界。
- 最优子结构,就是分析问题的关键,构建解题框架。之前以斐波那契数列为例,这次我们以这道题目为例。想知道到达n层台阶,有多少种方式,即f(n)是最终想要的答案,抽象为f(i) 即到达任何一层台阶的方式;所谓子结构,即f(i-1),f(i-2),f(i-3),...,f(2),f(1)。很明显,f(i)的方案数目和 f(i-1)等子问题的答案是有关系的。这就是第一步分析问题我们要做的。
- 状态转移方程。f(i)和子问题具体是什么关系呢?这一步要定量分析。由于每次只能走一步或两步,一步之前的台阶在i-1层,两步之前在i-2层,那么第i层的方案数目不就是:f(i) = f(i-1) + f(i-2),(还是斐波那契数列)。这一步定量分析问题和子问题之间的数量关系,
- 边界。到这儿就很好理解了,可以定义 f(1)=1, f(2)=2.
- 其实还有第四步,就是编码啦,能把你的思路写出来才行。
下面我们看代码实现,在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]