动态规划
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
主要思想
若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。动态规划往往用于优化递归问题,例如斐波那契数列,如果运用递归的方式来求解会重复计算很多相同子问题,利用动态规划的思想可以减少计算量。
动态规划法仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量,一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。
动态规划模板步骤:
- 确定动态规划状态
- 写出状态转移方程(画出状态转移表)
- 考虑初始化条件
- 考虑输出状态
- 考虑对时间,空间复杂度的优化(Bonus)
解题思路
-
第一步:确定动态规划状态
是否存在状态转移?
什么样的状态比较好转移,找到对求解问题最方便的状态转移?
想清楚到底是直接用需要求的,比如长度作为dp保存的变量还是用某个判断问题的状态比如是否是回文子串来作为方便求解的状态。该题目可以直接用一个一维数组dp 来存储转移状态,dp[i] 可以定义为以nums[i] 这个数结尾的最长递增子序列的长度。举个实际例子,比如在nums[10,9,2,5,3,7,101,18] 中,dp[0] 表示数字10的最长递增子序列长度,那就是本身,所以为1,对于dp[5] 对应的数字7来说的最长递增子序列是[2,5,7] (或者[2,3,7] )所以dp[5]=3 。
-
第二步: 写出规划转移方程:
由于是判断最长子序列的长度且并没有标明该子序列需要时连续的, 所以判断时以nums[i] 为结尾计算dp[i]的值:
for i in range (len(nums)):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = //record the situation
- 第三步: 考虑初始化条件
由于最低可能的子序列长度为 1, 所以dp数组初始化为
dp = [1] * len(nums)
- 第四步: 考虑输出形式呢
如果在进行第二层循环时发现nums[j] < nums[i] , 那么此时dp[i]所要记录的值只需要将dp[j]的值+ 1 即可,如果不大于或者等于那dp[i]依旧等于原来记录的值,所以输出形式可以为
dp[i] = max(dp[i], dp[j] + 1)
- 最后一步:考虑对时间空间复杂度优化:
我们看到,之前方法遍历dp列表需要 ,计算每个dp[i] 需要 的时间,所以总复杂度是O(N2)
前面遍历dp列表的时间复杂度肯定无法降低了,但是我们看后面在每轮遍历[0,i] 的dp[i] 元素的时间复杂度可以考虑设计状态定义,使得整个dp为一个排序列表,这样我们自然想到了可以利用二分法来把时间复杂度降到了O(NlogN) 。这里由于篇幅原因,如果大家感兴趣的话详细的解题步骤可以看好心人写的二分+动态规划
解答:
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) #确定输出状态
算法应用
解答:
def findLengthOfLCIS(self, nums: List[int]) -> int:
if not nums:return 0 #判断边界条件
dp=[1]*len(nums) #初始化dp数组状态
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)
解答:
def longestPalindrome(self, s: str) -> str:
length=len(s)
if length<2: #判断边界条件
return s
dp=[[False for _ in range(length)]for _ in range(length)]
max_len=1
start=0 #后续记录回文串初试位置
for j in range(1,length):
for i in range(j):
#矩阵中逐个遍历
if s[i]==s[j]:
if j-i<3:
dp[i][j]=True
else:
dp[i][j]=dp[i+1][j-1]
if dp[i][j]: #记录位置,返回有效答案
cur_len=j-i+1
if cur_len>max_len:
max_len=cur_len
start=i
return s[start:start+max_len]
解答:
def longestPalindromeSubseq(self, s: str) -> int:
n=len(s)
dp=[[0]*n for _ in range(n)] #定义动态规划状态转移矩阵
for i in range(n): # 初始化对角线,单个字符子序列就是1
dp[i][i]=1
for i in range(n,-1,-1): #从右下角开始往上遍历
for j in range(i+1,n):
if s[i]==s[j]: #当两个字符相等时,直接子字符串加2
dp[i][j]= dp[i+1][j-1]+2
else: #不相等时,取某边最长的字符
dp[i][j]=max(dp[i][j-1],dp[i+1][j])
return dp[0][-1] #返回右上角位置的状态就是最长
解答:
def minDistance(self, word1, word2):
#m,n 表示两个字符串的长度
m=len(word1)
n=len(word2)
#构建二维数组来存储子问题
dp=[[0 for _ in range(n+1)] for _ in range(m+1)]
#考虑边界条件,第一行和第一列的条件
for i in range(n+1):
dp[0][i]=i #对于第一行,每次操作都是前一次操作基础上增加一个单位的操作
for j in range(m+1):
dp[j][0]=j #对于第一列也一样,所以应该是1,2,3,4,5...
for i in range(1,m+1): #对其他情况进行填充
for j in range(1,n+1):
if word1[i-1]==word2[j-1]: #当最后一个字符相等的时候,就不会产生任何操作代价,所以与dp[i-1][j-1]一样
dp[i][j]=dp[i-1][j-1]
else:
dp[i][j]=min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1 #分别对应删除,添加和替换操作
return dp[-1][-1] #返回最终状态就是所求最小的编辑距离
解答:
def rob(self, nums):
if(not nums): #特殊情况处理
return 0
if len(nums)==1:
return nums[0]
n=len(nums)
dp=[0]*n #初始化状态转移数组
dp[0]=nums[0] #第一个边界值处理
dp[1]=max(nums[0],nums[1])#第二个边界值处理
for i in range(2,n):
dp[i]=max(dp[i-2]+nums[i],dp[i-1]) #状态转移方程
return dp[-1]
解答:
def rob(self, nums: List[int]) -> int:
if not nums:
return 0
elif len(nums)<=2:
return max(nums)
def helper(nums):
if len(nums)<=2:
return max(nums)
dp=[0]*len(nums)
dp[0]=nums[0]
dp[1]=max(nums[0],nums[1])
for i in range(2,len(nums)):
dp[i]=max(dp[i-1],dp[i-2]+nums[i])
return dp[-1]
return max(helper(nums[1:]),helper(nums[:-1]))