动态规划
若要解一个给定问题,我们需要解其不同的部分(即子问题),再根据子问题的解以得出原问题的解。动态规划往往用于优化递归归类,例如斐波那契数列,如果运用递归的方式来弥补会重复计算很多相同的子问题,利用动态规划的思想可以减少计算量。
动态规划法仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量,
一旦有人给定子问题的解已经算出,则将其记忆化存储,刹车下一步需要同一个子问题解之时直接查表。
动态规划模板步骤:
确定动态规划状态
写出状态转移方程(画出状态转移表)
考虑初始化条件
考虑输出状态
考虑对时间,空间复杂度的优化(奖金)
300.最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
1.确定动态规划状态
- 是否存在状态转移?
- 什么样的状态比较好转移,找到对求解问题最方便的状态转移?
想清楚到底是直接用需要求的,比如长度作为dp保存的变量还是用某个判断问题的状态比如是否是回文子串来作为方便求解的状态
2.写出一个好的状态转移方程
*使用数学归纳法思维,写出准确的状态方程
3.考虑初始条件
这是决定整个程序能否跑通的重要步骤,当我们确定好状态转移方程,我们就需要考虑一下边界值,边界值考虑主要又分为三个地方:
dp数组整体的初始值
dp数组(二维)i=0和j=0的地方
dp存放状态的长度,是整个数组的长度还是数组长度加一,这点需要特别注意。
额外总结几种Python常用的初始化方法:
对于产生一个全为1,长度为n的数组:
1. dp=[1 for _ in range(n)]
2. dp=[1]*n
对于产生一个全为0,长度为m,宽度为n的二维矩阵:
1. dp=[[0 for _ in range(n)] for _ in range(m)]
2. dp=[[0]*n for _ in range(m)]
4.考虑输出状态
主要有以下三种形式,对于具体问题,我们一定要想清楚到底dp数组里存储的是哪些值,最后我们需要的是数组中的哪些值:
返回dp数组中最后一个值作为输出,一般对应二维dp问题。
返回dp数组中最大的那个数字,一般对应记录最大值问题。
返回保存的最大值,一般是Maxval=max(Maxval,dp[i])这样的形式。
最终代码
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:
return 0
dp=[1]*len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[i]>nums[j]:
dp[i]=max(dp[j]+1,dp[i])
return max(dp)
5.考虑对时间,空间复杂度的优化(Bonus)
切入点: 我们看到,之前方法遍历dp列表需要
O
(
N
)
O(N)
O(N),计算每个dp[i]需要
O
(
N
)
O(N)
O(N)的时间,所以总复杂度是
O
(
N
2
)
O(N^2)
O(N2)
模板总结:
for i in range(len(nums)):
for j in range(i):
dp[i]=最值(dp[i],dp[j]+...)
674.最长连续递增序列
题目描述
给定一个未经排序的整数数组,找到最长且连续的的递增序列。
示例 1:
输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。
解题思路
这道题是不是一眼看过去和上题非常的像,没错了,这个题目最大的不同就是连续两个字,这样就让这个问题简单很多了,因为如果要求连续的话,那么就不需要和上题一样遍历两遍数组,只需要比较前后的值是不是符合递增的关系。
第一步:确定动态规划状态 对于这个问题,我们的状态dp[i]也是以nums[i]这个数结尾的最长递增子序列的长度
第二步:写出状态转移方程 这个问题,我们需要分两种情况考虑,第一种情况是如果遍历到的数nums[i]后面一个数不是比他大或者前一个数不是比他小,也就是所谓的不是连续的递增,那么这个数列最长连续递增序列就是他本身,也就是长度为1。 第二种情况就是如果满足有递增序列,就意味着当前状态只和前一个状态有关,dp[i]只需要在前一个状态基础上加一就能得到当前最长连续递增序列的长度。总结起来,状态的转移方程可以写成 dp[i]=dp[i-1]+1
第三步:考虑初始化条件 和上面最长子序列相似,这个题目的初始化状态就是一个一维的全为1的数组。
第四步:考虑输出状态 与上题相似,这个问题输出条件也是求dp数组中最大的数。
第五步:考虑是否可以优化 这个题目只需要一次遍历就能求出连续的序列,所以在时间上已经没有可以优化的余地了,空间上来看的话也是一维数组,并没有优化余地。
综上所述,可以很容易得到最后的代码:
def findLengthOfLCIS(self, nums: List[int]) -> int:
if not nums:return 0 #判断边界条件
dp=[1]*len(nums) #初始化dp数组状态
#注意需要得到前一个数,所以从1开始遍历,否则会超出范围
for i in range(1,len(nums)):
if nums[i]>nums[i-1]:#根据题目所求得到状态转移方程
dp[i]=dp[i-1]+1
else:
dp[i]=1
return max(dp) #确定输出状态
总结: 通过这个题目和例题的比较,我们需要理清子序列和子数组(连续序列)的差别,前者明显比后者要复杂一点,因为前者是不连续的序列,后者是连续的序列,从复杂度来看也很清楚能看到即使穷举子序列也比穷举子数组要复杂很多。