1 理解动态规划
首先看定义:
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
从定义中至少可以看出来三点要点:
1.动态规划其实是分治算法的一种
2.动态规划中动态的意思是要解决的问题,其规模不确定,而问题的解却依赖于问题规模
3.能以较快速度(动态规划是一种泛用性算法,而泛用性算法与特定算法相比往往存在性能差距)将结果正确计算出来
动态规划效率高的原因,是以空间换取时间。将前一步的结果储存起来,下一步可以直接调用,加速了计算速度。
引用其他文章中的理解:动态规划这一思想的实质其实是以下两点:
1.分析问题,构造状态转移方程
2.以空间换时间
说下自己的理解:
动态规划是求数列的题目,数列有三个重要元素,初始值,递推式(通项公式),求和式
高中数学题目中,有数列的内容,数列的推导式有两种,一种类似于An=3*n-1,这个是公式法,即通项公式,当知道明确规模n时,可以直接求出An的值
第二种是递推式,即An=An-1+3,这个是递推式,描述前后两项关系,及一种规律,通过这个递推式,同样能解决关于数列的所有信息。
但数学的数列问题,都是欠缺实际含义的,会给出一些已存在的项,及项之间的关系,基本就是幼儿园水平,拿来直接找规律就行。
动态规划的数列问题,则是数列问题的实际运用,至少大学水平(哈哈,调侃一下)。需要1.自己确定数列的含义(确定动态规划状态),即你的数列的值,表示什么意思,存的是啥东西。2.写出状态转移方程,就是递推公式(这个递推公式一般带条件判断)。3.考虑初始化条件(求A0,任何数列问题都需要确定初始值)
这部分基本就是数列问题和动态规划的相似之处了,如何忘记数列知识,可以回去翻高中数学教材。
所以动态规划,可以说是数列思想的计算算法实现,后面还涉及算法优化,难度是很大的。
有了数列,那么下一步就是从数列取要的答案了,这里有几种情况,需要根据题意选择,应该是比较简单的,要么是特定规模的值,要么是数列中的某个极值。
严谨一点表达,就是
返回dp数组中最后一个值作为输出,一般对应二维dp问题。
返回dp数组中最大的那个数字,一般对应记录最大值问题。
返回保存的最大值,一般是Maxval=max(Maxval,dp[i])这样的形式。
最后一步,则是计算机算法都会有的一点东西,算法优化,涉及存储优化。
2 动态规划解决问题的步骤
有了以上理解,那么看一道具体问题的求解过程
第一步:确定动态规划状态
是否存在状态转移?
什么样的状态比较好转移,找到对求解问题最方便的状态转移?
想清楚到底是直接用需要求的,比如长度作为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数组,我们思考一下是如何得到dp[5]=3的:既然是递增的子序列,我们只要找到nums[5] (也就是7)前面那些结尾比7小的子序列,然后把7接到最后,就可以形成一个新的递增的子序列,也就是这个新的子序列也就是在找到的前面那些数后面加上7,相当长度加1。当然可能会找到很多不同的子序列,比如刚刚在上面列举的,但是只需要找到长度最长的作为dp[5]的值就行。总结来说就是比较当前dp[i]的长度和dp[i]对应产生新的子序列长度,我们用j来表示所有比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])这样的形式。
**Tips:**这个公式必须是在满足递增的条件下,也就是nums[i]>nums[j]的时候才能成立,并不是nums[i]前面所有数字都满足这个条件的,理解好这个条件就很容易懂接下来在输出时候应该是max(dp)而不是dp[-1],原因就是dp数组由于计算递增的子序列长度,所以dp数组里中间可能有值会是比最后遍历的数值大的情况,每次遍历nums[j]所对应的位置都是比nums[i]小的那个数。举个例子,比如nums=[1,3,6,7,9,4,10,5,6],而最后dp=[1,2,3,4,5,3,6,4,5]。 总结一下,最后的结果应该返回dp数组中值最大的数。
最后加上考虑数组是否为空的判断条件,下面是该问题完整的代码:
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) #确定输出状态
实战
5. 最长回文子串
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
dp = [[False] * n for _ in range(n)]
ans = ""
# 枚举子串的长度 l+1
for l in range(n):
# 枚举子串的起始位置 i,这样可以通过 j=i+l 得到子串的结束位置
for i in range(n):
j = i + l
if j >= len(s):
break
if l == 0:
dp[i][j] = True
elif l == 1:
dp[i][j] = (s[i] == s[j])
else:
dp[i][j] = (dp[i + 1][j - 1] and s[i] == s[j])
if dp[i][j] and l + 1 > len(ans):
ans = s[i:j+1]
return ans
72.编辑距离
class Solution:
def minDistance(self, word1, word2):
"""
:type word1: str
:type word2: str
:rtype: int
"""
n = len(word1)
m = len(word2)
# 有一个字符串为空串
if n * m == 0:
return n + m
# DP 数组
D = [ [0] * (m + 1) for _ in range(n + 1)]
# 边界状态初始化
for i in range(n + 1):
D[i][0] = i
for j in range(m + 1):
D[0][j] = j
# 计算所有 DP 值
for i in range(1, n + 1):
for j in range(1, m + 1):
left = D[i - 1][j] + 1
down = D[i][j - 1] + 1
left_down = D[i - 1][j - 1]
if word1[i - 1] != word2[j - 1]:
left_down += 1
D[i][j] = min(left, down, left_down)
return D[n][m]
- 打家劫舍
class Solution:
def rob(self, nums: List[int]) -> int:
if not nums:
return 0
size = len(nums)
if size == 1:
return nums[0]
dp = [0] * size
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, size):
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
return dp[size - 1]
- 打家劫舍II
class Solution:
def rob(self, nums: [int]) -> int:
def my_rob(nums):
cur, pre = 0, 0
for num in nums:
cur, pre = max(pre + num, cur), cur
return cur
return max(my_rob(nums[:-1]),my_rob(nums[1:])) if len(nums) != 1 else nums[0]
- 最长回文子序列
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n = len(s)
maxL = -1
dp = [[0]*n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
for i in range(n-1, -1, -1):
for j in range(i+1, n):
if s[i] == s[j]:
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][n-1]
- 最长连续递增序列
class Solution(object):
def findLengthOfLCIS(self, nums):
ans = anchor = 0
for i in range(len(nums)):
if i and nums[i-1] >= nums[i]: anchor = i
ans = max(ans, i - anchor + 1)
return ans