动态规划【82-91】
动态规划5步曲
- 确定dp数组 & 下标含义
- 递推公式
- dp数组初始化 【涉及乘法,dp[0] =1 】
- 遍历顺序
- 举例推导dp数组
背包问题
- 01背包(每件物品只能用0或1次):n件物品,最多装重量w的背包,第i件物品的重量为weight[i],价值value[i],每件物品只能装1次,求背包价值最大总和
- 二维dp, dp[i][j]:从下标[0,i]任取,放进容量为j的背包,价值最大为dp[i][j]
dp[i][j] = max( dp[i-1][j] , dp[i-1][j-weight[i]]+ value[i]) 拿不拿第i个物品- 一维dp, 先遍历物品i, 在倒序遍历容量j【倒序是为了保证:物品只拿一次】
dp[j] = max(dp[j], dp[j-weight[i]] + value[i])
- 完全背包(每个物品数量无限)
先遍历物品i,或者先正序遍历物品j都行
【组合数:外层for物品,内层for容量】【排列数:外层for容量,内层for物品】
打家劫舍 dp[i]:下标i内(包括i)的房子,最多偷的金额为dp[i]
- 2个相邻都被偷,会报警 dp[i] = max(dp[i-1], dp[i-2]+nums[i]) 其实就是nums[i]偷不偷
- 房屋成环 res1 = rob(nums[:-1]) res2 = rob(nums[1:]) res = max(res1, res2)
- 房屋树形 dp[0]代表不偷该节点,得到的最大钱, dp[1]代表偷该节点,得到的最大钱
股票买卖的最佳时机dp[i][0]第i天持有股票,所获最大钱 dp[i][1]第i天不持有股票,所获最大钱
- 只能买卖1次 dp[i][0] = max(d[i-1][0], -price[i]) dp[i][1] = max(dp[i-1][0]. dp[i-1][0]+price[i])
- 多次买卖 dp[i][0] = max(d[i-1][0], -price[i] + dp[i-1][1])
- 只能买卖2次
- 只能买卖k次 dp[i][j]【 j=0不操作, j= 2k-1第k次买入,j=2k第k次卖出 】
- 含有冷冻期 dp[i][j]【 j=0持有股票, j=1不持有股票也不在冷冻期,也不是刚卖,j=2冷冻期,j=3刚卖出 】
- 卖出有手续费 dp[i][1] = max(dp[i-1][0]. dp[i-1][0]+price[i]-fee)
子序列问题
- 在一个序列里面:求最长递增子序列长度【 dp[i]:子序列答案以nums[i]结尾的最长递增子序列的长度】
1.1 不连续 for i,遍历j<i, 如果nums[i] > nums[j],dp[i] = max(dp[i], dp[j]+1)
1.2 连续 if num[i[ > nums[i-1]:dp[i] = dp[i-1]+1
【求连续数组的最大和】 dp[i] = max(dp[i-1]+nums[i], nums[i])- 两个序列:求最长公共子序列的长度【 dp[i][j]:A[0,i-1]和B[0,j-1]最长连续公共子序列的长度】
2.1 不连续 if A[i-1]== B[j-1]:dp[i][j] = dp[i-1][j-1]+1 else:dp[i][j] = max(dp[i-1][j], dp[i][j-1])
2.2 连续 if A[i-1]== B[j-1]:dp[i][j] = dp[i-1][j-1]+1回文子串
70.爬楼梯
'''
# dp[i]:爬到第i个楼梯,有dp[i]种办法
# dp[i] = dp[i-1] + dp[i-2]
# dp[1] = 1 dp[2] = 2
'''
class Solution:
def climbStairs(self, n: int) -> int:
if n<=2 :
return n
dp = [1] * (n+1)
dp[2] = 2
for i in range(3, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
118.杨辉三角
'''
1.dp[i]:杨辉三角第i行
2.dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
3.dp每一排的第一个数和最后一个数都是 1,即 dp[i][0]=dp[i][i]=1
4.从上到下,从左到右
'''
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
dp = [[1]*(i+1) for i in range(numRows)]
for i in range(2, numRows):
for j in range(1, i):
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
return dp
198.打家劫舍
'''
1.dp[i]:到下标i户,偷最大金额
2.dp[i] = max(dp[i-1], dp[i-2]+num[i-1]) #第i家偷不偷,偷的话:dp[i-2]+num[i-1],不偷的话:dp[i-1]
3.dp[0]= nums[0] dp[1]= max(nums[0],nums[1])
4.i从左到右
'''
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
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[len(nums)-1]
279.完全平方数,最少完全平方数
完全背包问题
'''
1.dp[i] : 和为i,完全平方数的最小数量
2. 变量j【从小到大】 dp[i] = min(dp[i-j**2]+1,dp[i])
3.dp[0] = 0,其他要初始化为最大值
'''
class Solution:
def numSquares(self, n: int) -> int:
dp = [float('inf')] * (n+1)
dp[0] = 0
for i in range(1, n+1):
for j in range(1, int(i**(1/2))+1):
dp[i] = min(dp[i-j**2]+1, dp[i])
return dp[n]
322.零钱兑换,最少硬币数
完全背包
'''
1.dp[i] : 和为i,凑成i所需的 最少的硬币个数
2. 遍历j, dp[i] = min(dp[i-coins[j]]+1,dp[i])
3.dp[0] = 0,其他要初始化为最大值
'''
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount+1)
dp[0] = 0
# dp[j]:凑成总金额j所需的最少的硬币个数
for i in coins:
for j in range(1, amount+1):
if j>=i:
dp[j] = min(dp[j-i]+1, dp[j])
if dp[amount] == float('inf'):
return -1
return dp[-1]
139.单词拆分
完全背包
'''
1.dp[i]: s[0:i]能不能被worddict拼接
2.dp[j] == True and s[j:i] in wordset: dp[j] = True break
3.dp[0]=True ,其他全部为False
'''
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
wordset = set(wordDict)
n = len(s)
dp = [False] * (n+1)
dp[0] = True
for i in range(1, n+1):
for j in range(i):
if dp[j] == True and s[j:i] in wordset:
dp[i] = True
break
return dp[n]
300.最长递增子序列
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if len(nums) <= 1:
return len(nums)
dp = [1] * len(nums) #下标为0-i的最长严格子序列长度
dp[0] = 1
res = 1
for i in range(1, len(nums)):
for j in range(0, i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j]+1)
res = max(res, dp[i])
return res
152.乘积最大子数组
我们只要记录前 i 的最小值,和最大值,那么 dp[i] = max(nums[i] * pre_max, nums[i] * pre_min, nums[i]),这里 0 不需要单独考虑,因为当相乘不管最大值和最小值,都会置 0
什么时候不连续:当max,min取到i,代表新的开始
class Solution:
def maxProduct(self, nums: List[int]) -> int:
if not nums:
return
res = nums[0]
pre_max = nums[0]
pre_min = nums[0]
for i in nums[1:]:
cur_max = max(pre_max*i, pre_min*i, i)
cur_min = min(pre_max*i, pre_min*i, i)
res = max(res, cur_max)
pre_max = cur_max
pre_min = cur_min
return res
416.分割等和子集
class Solution:
def canPartition(self, nums: List[int]) -> bool:
'''
就是看dp[sum/2]== sum//2,01背包
1. dp[i]: 容量为i的背包,最多装载
2. dp[i] = max(dp[i-num]+num, dp[i])
3. dp[0] = 0
'''
sums = sum(nums)
dp = [0] * (sums//2+1)
if sums%2 == 1:
return False
for i in nums:
for j in range(sums//2, i-1, -1):
dp[j] = max(dp[j], dp[j-i]+i)
return dp[sums//2] == sums//2
32.最长有效括号
如果成对,栈就pop
class Solution:
def longestValidParentheses(self, s: str) -> int:
stack=[] # 构建一个栈记录字符index
ans=0
for i in range(len(s)):
# 如果栈非空,且当前为右括号,且有记录的左括号,则出栈
if stack and s[i]==")" and s[stack[-1]]=="(":
stack.pop()
# 如果出栈后变成空栈,则说明整个[0:i]的区间都是合格的,长度为i+1
# 如果出栈后非空,则说明区间[stack[-1]:i]是合格的
ans=max(ans,i-(stack[-1] if stack else -1))
else:
# 以下3个条件会触发else
# 如果是空栈
# 或者当前字符为左括号(需要寻找匹配的右括号)
# 或者当前字符为右括号,而且栈顶记录的也是右括号(不合格的情况,永远不会被pop)
stack.append(i)
return ans
多维动态规划【91-95】
62.不同路径
'''
1. dp[i][j]:到达下标{i,j},总共的路径
2.dp[i][j] = dp[i-1][j] + dp[i][j-1]
3. dp[0][i] = d[0][j] = 1
'''
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[0] * n for _ in range(m)]
for i in range(m):
dp[i][0] = 1
for j in range(n):
dp[0][j] = 1
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[m - 1][n - 1]
64.最小路径和
'''
1. dp[i][j]:到下标 (i,j),数字总和最小
2. dp[i][j] = min(dp[i][j-1], dp[i-1][j]) + grid[i][j]
3. dp[0][0] = grid[0][0]
dp[i][0] = sum(grid[i][0]) dp[0][j] = sum(grid[0][j])
'''
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m = len(grid)
n = len(grid[0])
dp = [[0]*n for _ in range(m)]
dp[0][0] = grid[0][0]
for i in range(1,m):
dp[i][0] = grid[i][0] + dp[i-1][0]
for j in range(1,n):
dp[0][j] = grid[0][j] + dp[0][j-1]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i][j-1], dp[i-1][j]) + grid[i][j]
return dp[-1][-1]
5.最长回文子串
'''
1.dp[i][j]:区间[i:j]左闭右闭的子串,是否是回文子串
2.如果s[i] != s[j], dp[i][j] = False
如果s[i]== s[j]【1.下标相同,是回文子串 ;
2.下标相差为1,‘aa’,也是回文;
3.下标相差大于1,如果dp[i+1][j-1]也是回文,才是回文】
3.初始化:dp[i][j] = False
4.i从大到小, j从小到大
'''
class Solution:
def longestPalindrome(self, s: str) -> str:
dp = [[False] * len(s) for _ in range(len(s))]
maxlenth = 0
left = 0
right = 0
for i in range(len(s) - 1, -1, -1):
for j in range(i, len(s)):
if s[j] == s[i]:
if j - i <= 1 or dp[i + 1][j - 1] == True:
dp[i][j] = True
if dp[i][j] and j - i + 1 > maxlenth:
maxlenth = j - i + 1
left = i
right = j
return s[left:right + 1]
1143.最长公共子序列
'''
1. dp[i][j]:以text1[i-1]结尾,text2[j-1]结尾,最长公共序列的长度
2. 如果text2[i-1] == text1[j-1], dp[i][j] = dp[i-1][j-1] + 1
如果text2[i-1] != text1[j-1], dp[i][j]=max(dp[i-1][j], dp[i][j-1])
3.dp[i][0]; dp[0][j]无意义
'''
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
if len(text1) == 0 or len(text2) == 0:
return 0
dp = [[0] * (len(text1)+1) for _ in range(len(text2)+1)]
for i in range(1, len(text2)+1):
for j in range(1, len(text1)+1):
if text2[i-1] == text1[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j]=max(dp[i-1][j], dp[i][j-1])
return dp[-1][-1]
72.编辑距离
'''
1. dp[i][j] : 将word1[0:i]转变成word2[0:j],最少操作数
2. 如果 word1[i-1]==word2[j-1]: dp[i][j] = dp[i-1][j-1]
如果 word[i-1]!=word2[j-1]: dp[i][j] = min(dp[i-1][j], dp[i][j-1],dp[i-1][j-1])+1
3.dp[0][j]=j dp[i][0] = i
'''
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
dp = [[0] * (len(word2)+1) for _ in range(len(word1)+1) ]
for i in range(len(word2)+1):
dp[0][i] = i
for i in range(len(word1)+1):
dp[i][0] = i
for i in range(1, len(word1)+1):
for j in range(1, len(word2)+1):
if word2[j-1]==word1[i-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[len(word1)][len(word2)]