群内编号:2-木铎铎
博文内容学习自DataWhale开源文档:
LeetCode分类训练 Task 2 动态规划
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
主要思想
要解一个给定问题,需解其不同子问题,再根据子问题的解得出原问题的解。动态规划(DP)往往用于优化递归问题,如斐波那契数列,运用递归的方式求解会重复计算很多相同子问题,利用DP思想可减少计算量。
DP仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量。一旦某个给定子问题的解已经算出,则将其记忆化存储,下次需要同一个子问题的解时查表即可。
动态规划模板步骤:
-
确定动态规划状态
-
写出状态转移方程(画出状态转移表)
-
考虑初始化条件
-
考虑输出状态
-
考虑对时间,空间复杂度的优化(Bonus)
经典案例 1:Leetcode 300.最长上升子序列
题目描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n^2) 。
第一步:确定动态规划状态
该题目可以直接用一个一维数组dp来存储转移状态,dp[i]
可以定义为以nums[i]
这个数结尾的最长递增子序列的长度。
第二步:写出状态转移方程
for i in range(len(nums)):
for j in range(i):
if nums[i]>nums[j]:
dp[i]=max(dp[i],dp[j]+1)
Tips:在实际问题中,如果不能很快得出这个递推公式,可以先尝试一步一步把前面几步写出来,如果还是不行很可能就是 dp 数组的定义不够恰当,需要回到第一步重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。
第三步:考虑初始化条件
这是决定整个程序能否跑通的重要步骤,当我们确定好状态转移方程,我们就需要考虑一下边界值,边界值考虑主要又分为三个方面:
-
dp数组整体的初始值
-
dp数组(二维)i=0和j=0的地方
-
dp存放状态的长度,是整个数组的长度还是数组长度加一,这点需要特别注意。
对于本问题,子序列最少也是自己,所以长度为1,这样我们就可以方便的把所有的dp初始化为1,再考虑长度问题,由于dp[i]
代表的是nums[i]
的最长子序列长度,所以并不需要加一。 所以用代码表示就是dp=[1]*len(nums)
。
第四步:考虑输出状态
主要有以下三种形式,对于具体问题,我们一定要想清楚到底dp数组里存储的是哪些值,最后我们需要的是数组中的哪些值:
-
返回dp数组中最后一个值作为输出,一般对应二维dp问题。
-
返回dp数组中的最大值,一般对应记录最大值问题。
-
返回保存的最大值,一般是
Maxval=max(Maxval,dp[i])
这样的形式。
最后加上考虑数组是否为空的判断条件,下面是该问题完整的代码:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:return 0 #判断边界条件
dp=[1]*len(nums) #初始化dp数组状态
for i in range(len(nums)):
for j in range(i):
if nums[i]>nums[j]: #根据题目所求得到状态转移方程
dp[i]=max(dp[i],dp[j]+1)
return max(dp) #确定输出状态
第五步:考虑对时间,空间复杂度的优化(Bonus)
请参见:
二分方法+动态规划详解
经典案例 1:Leetcode5. 最长回文子串
题目描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
第一步:确定动态规划状态
这里定义dp[i][j]
表示子串s从i到j是否为回文子串。
第二步:写出状态转移方程
# 字符串首尾两个字符必须相等,否则肯定不是回文
if s[i]==s[j]:
# j-1-(i+1)+1<2
if j-i<3:
dp[i][j]=True
else:
dp[i][j]=dp[i+1][j-1]
第三步:考虑初始化条件
由于只有一个字符的时候肯定是回文串,所以dp表格的对角线dp[i][i]
肯定是True。
第四步:考虑输出状态
这里dp表示的是从i到j是否是回文子串,这样一来就告诉我们子串的起始位置和结束位置。
if dp[i][j]:
# 只要dp[i][j]成立就表示是回文子串,之后记录位置,返回有效答案
cur_len=j-i+1
if cur_len>max_len:
max_len=cur_len
start=i
第五步:考虑对时间,空间复杂度的优化
请参考下面的题解:
动态规划、Manacher 算法
结语
动态规划除了解决子序列问题,也可以用来解决其他实际的问题,比如一些AI的经典算法。本文只简单记录了一些基础的用法和2道经典的题目,更详细的分析和更多的例子请见原文(地址在本文顶部)。日后对DP算法还要多多应用,多多理解^ _ ^