动态规划
解题步骤
如果按照DP个标准型表述,可以写成如下形式:
1)状态定义:一般题目要什么,就把状态定义成什么
2)状态转移方程
3)初始状态
4)返回值
示例汇总
1.面试题42. 连续子数组的最大和
状态转移方程:
如果dp[i-1]<0,则dp[i]=nums[i]
否则dp[i]=dp[i-1]+nums[i]
2.面试题 08.11. 零钱兑换
相同题目( .518. 零钱兑换 II)
状态转移方程:(注意:顺序无关,1+5和5+1是同一种方案)
先遍历零钱,保证在考虑一枚零钱的情况时,没有较大的零钱影响,这样,我们最终每种组合情况,都是以零钱的面额大小非递减组合。保证了同样的情况,调换顺序后重复计算的情况。DP状态的变化通过多轮计算完成。
coins=[1,5,10,25]
dp= [0 for i in range(n+1)]
dp[0]=1
for coin in coins:
for i in range(coin,n+1):
dp[i]=dp[i]+dp[i-coin]
3.337. 打家劫舍 III:三种解法,递归、递归+记忆化、后序遍历+动态规划
状态转移方程思路:
- 当前节点是否打劫,受两个叶子节点是否打劫的影响,所以,应该先访问叶子结点,再确定当前节点是否打劫。所以,需要后序遍历。
- 当前节点是否打劫,与左右叶子结点是否打劫所累计的金额有关。所以,需要用一个数组存储累计金额。
def dfs(root):
"""
"""
if not root:#root 节点不存在,即,偷不偷该点,最终返回值都为0
return [0,0]
leftArr=dfs(root.left)
rightArr=dfs(root.right)
#分别指代当前节点偷与不偷,累积到当前节点的最大偷取金额
dp=[0,0]
#当前节点不偷,左右节点可偷、可不偷,所以,去各节点可能性的最大值计算
dp[0]=max(leftArr[0],leftArr[1]) + max(rightArr[0],rightArr[1])
#当前节点偷,左右节点只能不偷
dp[1]=leftArr[0]+rightArr[0]+root.val
return dp
4.279. 完全平方数:动态规划
从小到大开始访问,状态转移方程:dp[i]=min(dp[i], dp[i-square[j]]+1)
def numSquares(self, n):
dp=[n]*(n+1)
dp[0]=0
square=[i**2 for i in range(int(n**0.5)+1)]
for i in range(1,n+1):
for j in range(len(square)):
if square[j]<=i:
dp[i]=min(dp[i], dp[i-square[j]]+1)
else:
break
return dp[n]
5.152. 乘积最大子数组:动态规划
关键点:
- 需要分情况讨论为正、为负的情况,而且要从小到大的顺序访问index并记录正负累计乘积,因此,需要一个二维数组,消除后效性。
- 最终取值需要存在不同维度上,最后需要对他们再做比较。总结的规律:如果全部状态都以[0]做起点,使用最后状态取值就可以了,e.g.爬楼梯问题、198. 打家劫舍、213. 打家劫舍 II;如果从中间开始作为起点,需要再做对比,e.g.本题(乘积最大子数组)、连续子数组最大和。
- 状态方程:判断 “上轮状态与当前值乘积” VS “当前值” 两者的大小关系
if len(nums)==1:
return nums[0]
# 初始化
dp=[[0,0] for i in range(len(nums))]
#使用第二维记录max和min状态,便于分类讨论
dp[0][0]=nums[0] #min
dp[0][1]=nums[0] #max
for i in range(1,len(nums)):
#分类讨论
if nums[i]>0: #正值
dp[i][0] = dp[i - 1][0] * nums[i]
dp[i][1] = max(dp[i - 1][1] * nums[i],nums[i])
elif nums[i]<=0: #负值
dp[i][0] = min(dp[i - 1][1] * nums[i],nums[i])
dp[i][1] = max(dp[i - 1][0] * nums[i],nums[i])
m=nums[0]
for i in range(len(dp)):
m=m if m>dp[i][1] else dp[i][1]
return m
6.221. 最大正方形:动态规划+存储优化
关键点:
- 要点1:使用两个数组替代二维数组,用于存储DP状态
- 要点2:dp存储以(r,c)为右下角的最大正方形边长,它的大小与数组取值、附近3个dp位置的大小
- 状态转移方程:dp_cur[c] = min(dp_cur[c - 1], dp_pre[c - 1], dp_pre[c]) + 1 if(matrix[r][c]=="1") else 0
def maximalSquare(self, matrix):
"""
:type matrix: List[List[str]]
:rtype: int
"""
rows = len(matrix)
if(rows==0):return 0
cols = len(matrix[0])
if(cols==0):return 0
dp_pre = [0] * rows
dp_cur = [0] * cols
#首行的首列初始化
if (matrix[0][0] == "1"): dp_cur[0] = 1
#首行初始化
for c in range(1, cols):
dp_cur[c] = 1 if(matrix[0][c]=="1") else 0
max_val=max(dp_cur)
#动态规划状态转移
for r in range(1, rows):
dp_pre = dp_cur
dp_cur = [0] * cols
dp_cur[0] = 1 if(matrix[r][0] =="1") else 0
for c in range(1, cols):
#DP状态转移方程
dp_cur[c] = min(dp_cur[c - 1], dp_pre[c - 1], dp_pre[c]) + 1 if(matrix[r][c]=="1") else 0
max_val=max(max_val,max(dp_cur))
return max_val**2
7.264. 丑数 II:指针+自变量与变量同体
参考2
- 丑数乘以乘数「2、3、5」都为丑数,所以,可以用三个指针指向乘2\3\5的位置
- 关键在于要按照大小排列,因此,要有一个min(nums[i2]*2,nums[i3]*3,nums[i5]*5)的判断过程,丑数列表中每次只增加一个元素
- 之后,如果新增加的元素和之前丑数及对应乘数的乘积相等,则,丑数指针增加+1
#指针
i2=i3=i5=0
for i in range(1,1690+1):
if(i==n):
return nums[i-1]
nums.append(min(nums[i2]*2,nums[i3]*3,nums[i5]*5))
#注意,由于存在nums[i2]*2,nums[i3]*3,nums[i5]*5三者相同的情况,
#为了避免丑数重复出现,使用if,不要使用elif,只要符合条件都向前移动一次
if(nums[i]==nums[i2]*2):
i2=i2+1
if(nums[i]==nums[i3]*3):
i3=i3+1
if(nums[i]==nums[i5]*5):
i5=i5+1
return None
8.322. 零钱兑换
- 使用dp存储总金额为i时的需要的硬币数
- 针对不同面额的coin,对比dp[i], dp[i-coins[j]]+1的大小
for i in range(amount+1):
for j in range(n):
if coins[j] <= i:
dp[i] = min(dp[i], dp[i-coins[j]]+1)
9.474. 一和零
背包问题,采用动态规划经典做法,从小处展开。具体见代码。
空间复杂度为 O(lmn),总时间复杂度是 O(lmn + L)。
class Solution(object):
def findMaxForm(self, strs, m, n):
"""
:type strs: List[str]
:type m: int
:type n: int
:rtype: int
状态定义:dp[i][j][k],表示只考虑前i个字符串时,使用j个0和k个1,能够装进”背包“的最大字符串数量
状态转移方程:
dp[i][j][k] =
1)不考虑当前字符串,dp[i-1][j][k]
2)考虑当前字符串,假设当前字符串包含的0和1分别是j0和k0,dp[i-1][j-j0][k-k0]+1
"""
def getinfo(str_val):
c=[0,0]
for s in str_val:
if(s=='0'):
c[0]=c[0]+1
else:
c[1]=c[1]+1
return c[0],c[1]
l =len(strs)
dp=[[[0 for k in range(n+1)] for j in range(m+1)] for i in range(l+1) ]
for i in range(1,l+1):
j0,k0=getinfo(strs[i-1])
for j in range(0,m+1):
for k in range(0,n+1):
if(j>=j0 and k>=k0):
dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-j0][k-k0]+1)
else:
dp[i][j][k]=dp[i-1][j][k]
return dp[l][m][n]