目录
背包问题
2021/1/26
416. 分割等和子集
如果不定义第一列为true,就要对于每个i元素,赋值dp[i][nums[i]]=True
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if(len(nums)<2):
return False
# 求sum/2
numSum=0
for num in nums:
numSum+=num
# 奇数
if(numSum%2!=0):
return False
# 偶数
else:
numSum=int(numSum/2)
# dp[i][j] 表示前[0,i]个元素中能否取出一些元素sum=j
dp=[[False]*(numSum+1) for _ in range(len(nums))]
for i in range(len(nums)):
if(nums[i]<=numSum):
dp[i][nums[i]]=True
for j in range(1,numSum+1):
# S1:不拿i元素,dp[i-1][j]为true才可以
# S2:拿i元素,dp[i-1][j-元素i],为true才可以
# 不拿i元素
if(i>0):
dp[i][j]=dp[i-1][j] or dp[i][j]
# 拿i元素 由前提 j-nums[i]>=0
if(dp[i][j]==False and j-nums[i]>=0):
dp[i][j]=dp[i-1][j-nums[i]]
# 提前终止
if(dp[i][numSum]==True):
#print(dp)
return True
# 否则
#print(dp)
return False
- 直接命令第一列为0,后面就不用赋值了
- nums[i],分<=j,和>j两种情况考虑
- 先初始化第一行,然后每一行结果由上一行结果和nums[i]决定
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if(len(nums)<2):
return False
# 求sum/2
numSum=0
for num in nums:
numSum+=num
# 奇数
if(numSum%2!=0):
return False
# 偶数
else:
numSum=int(numSum/2)
# dp[i][j] 表示前[0,i]个元素中能否取出一些元素sum=j
dp=[[False]*(numSum+1) for _ in range(len(nums))]
# 初始化第一个元素的结果(第一行) 每一行的结果由上一行决定
dp[0][0]=True
if(nums[0]<=numSum):
dp[0]
for i in range(1,len(nums)):
dp[i][0]=True # 令第一列为true
for j in range(1,numSum+1):
# S1:不拿i元素,dp[i-1][j]为true才可以
# S2:拿i元素,dp[i-1][j-元素i],为true才可以
#元素i小于j,可拿可不拿i
if(nums[i]<=j):
dp[i][j]=dp[i-1][j-nums[i]] or dp[i-1][j]
#元素i大于j,不能拿i
else:
dp[i][j]=dp[i-1][j]
# 提前终止
if(dp[i][numSum]==True):
#print(dp)
return True
# 否则
#print(dp)
return False
优化空间复杂度
- 每一行的dp只与上一行的dp相关
- ==dp[i]表示抽出一些元素 sum等于i ==
- 遍历Nums:
- num>target 不用管
- num<=target 更新dp
- 只更新dp[target]到dp[num](因为dp[0]—dp[num-1]即sum,都比当前num小,当前num管不着它们的更新)
- 必须先更新大的,再更新小的,即倒序更新dp[i]
- dp[i]=dp[i] or dp[j-num] dp[i]等于当前状态,或者不取当前num时的dp[j-num]
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if(len(nums)<2):
return False
# 求sum/2
numSum=0
for num in nums:
numSum+=num
# 奇数
if(numSum%2!=0):
return False
# 偶数
else:
numSum=numSum//2
# dp[j]表示能否抽出部分数,sum=j
dp=[True]+[False]*numSum
for num in nums:
if(num>numSum):
continue
#当num<=target时 判断
for j in range(numSum,num-1,-1):
dp[j]=dp[j] or dp[j-num]
# 提前终止
if(dp[numSum]==True):
return True
return dp[numSum]
2021/1/28
494. 目标和
dp[i][j]表示前i个元素sum为j的次数
转移关系
- S1:当前num+,dp[i][j]+=dp[i-1][j-num]
- S2:当前num-,dp[i][j]+=dp[i-1][j+num]
class Solution:
def findTargetSumWays(self, nums: List[int], S: int) -> int:
numSum=0
for num in nums:
numSum+=num
if(S>numSum or S<-numSum):
return 0
# dp[i][j]表示前i+1个元素,组合sum=j的次数
dp=[[0]*(2*numSum+1) for _ in range(0,len(nums))]
# 转移关系
# S1:当前num+,dp[i][j]+=dp[i-1][j-num]
# S2:当前num-,dp[i][j]+=dp[i-1][j+num]
# 初始化第一行 numSum相当于sum=0的index
dp[0][numSum+nums[0]]+=1 # dp[numSum[0]]=0
dp[0][numSum-nums[0]]+=1 # 如果直接等于的话 第一个元素为0时,只会令其=1,实际=2,
for i in range(1,len(nums)):
for j in range(0,2*numSum+1):
# target=j-numSum #将j从index转化到target sum
# dp[i][j]+=dp[i-1][target-nums[i]+numSum]
# dp[i][j]+=dp[i-1][target+nums[i]+numSum]
if(j-nums[i]>=0 and j-nums[i]<=2*numSum):
dp[i][j]+=dp[i-1][j-nums[i]]
if(j+nums[i]>=0 and j+nums[i]<=2*numSum):
dp[i][j]+=dp[i-1][j+nums[i]]
#print(dp)
return dp[len(nums)-1][S+numSum]
优化空间复杂度,因为cur可以由左边+num得到,也可以由右边-num得到,所以需要两个一维数组
- Q:为什么 cur=L+R,不需要L+R+pre 呢??
class Solution:
def findTargetSumWays(self, nums: List[int], S: int) -> int:
numSum=0
for num in nums:
numSum+=num
if(S>numSum or S<-numSum):
return 0
# 空间复杂度优化
# dp[i]表示sum=i-numSum的次数
pre=[0]*(2*numSum+1)
# 初始化第一个元素
pre[numSum+nums[0]]+=1
pre[numSum-nums[0]]+=1
cur=[0]*(2*numSum+1)
# 转移关系
# S1:当前num+,dp[j]+=dp[j-num]
# S2:当前num-,dp[j]+=[j+num]
for num in nums[1::]:
for j in range(0,2*numSum+1):
L=0 # 从左边+num 要倒叙
R=0 # 从右边-num 要正序
if(j-num>=0 and j-num<=2*numSum):
L=pre[j-num]
if(j+num>=0 and j+num<=2*numSum):
R=pre[j+num]
#cur[j]=pre[j]+L+R #一直卡在这一条上了
cur[j]=L+R # cur为什么不用+pre呢??
pre=cur[::]
return pre[S+numSum]
大神解法,转化成subsum问题,真心厉害了。。
474. 一和零
两个背包,定义一个两个对应容量的dp[m][n]
dp[m][n]表示m个0字符,n个1字符容量下最大value,本题中即为str个数
对于每个str:
- ①拿当前str, dp[i][j]= dp[i][j]
- ②不拿当前str, dp[i][j]= dp[i-当前0个数][j-当前1个数]+1
- 实际 dp[i][j]取上述两种情况最大值
关键点:注意背包问题中倒叙更新
- 后面项由前面项决定 如果正序的话 会累加
- 比如 dp[3]=dp[2]+1 dp[2]=dp[1]+1 dp[1]=dp[0]+1 倒叙没问题,用的dp[i]都是本轮的值
- dp[1]=dp[0]+1 dp[2]=dp[1]+1 dp[3]=dp[2]+1 正序的话,单独看每个式子没问题,但是连着看,就变成累加了 dp[2]更新中的dp[1]是已经更新后的dp[1],不是本轮的值
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
# m+1行 n+1列 dp[i][j]表示 m=i n=j下的最大value(即元素数)
dp=[[0]*(n+1) for _ in range(m+1)]
for s in strs:
num0,num1=self.getCharNum(s)
# 其实下面两行不加也Ok
if(num0>m or num1>n):
continue
# 注意要倒叙排列
# 后面项由前面项决定 如果正序的话 会累加
for i in range(m,num0-1,-1):
for j in range(n,num1-1,-1):
# S1:不拿s;S2:拿s
dp[i][j]=max(dp[i][j],dp[i-num0][j-num1]+1)
return dp[m][n]
# 获取str中0和1的个数
def getCharNum(self,s):
num0=0
num1=0
for char in s:
if(char=='1'):
num1+=1
elif(char=='0'):
num0+=1
return num0,num1
2021/1/29
322. 零钱兑换
- dp[i]表示总金额i 最少硬币数量
- dp[i]只能由最小的dp[i-coin]+1得到
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
if(amount<0):
return -1
if(amount==0):
return 0
# dp[i]表示总金额i 最少硬币数量
dp=[0]*(amount+1)
# F(i)=min[F(i-coin1),F(i-coin2,...)]+1
for i in range(1,amount+1):
# 遍历coin
for coin in coins:
if(coin==i):
dp[i]=1
if(coin<i and dp[i-coin]>0):
if(dp[i]==0):
dp[i]=dp[i-coin]+1
else:
dp[i]=min(dp[i],dp[i-coin]+1)
#print(dp)
return dp[amount] if dp[amount]>0 else -1
官方给的,初始化dp为正无穷,dp[0]=0
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
if(amount<0):
return -1
# dp[i]表示总金额i 最少硬币数量
dp=[float('inf')]*(amount+1) #正无穷
dp[0]=0
# F(i)=min[F(i-coin1),F(i-coin2,...)]+1
for i in range(1,amount+1):
# 遍历coin
minPre=float('inf')
for coin in coins:
if(coin<=i):
minPre=min(minPre,dp[i-coin])
dp[i]=min(dp[i],minPre+1)
#print(dp)
return dp[amount] if dp[amount]!=float('inf') else -1
2021/3/1
518. 零钱兑换 II
- 先遍历所有的硬币,对于每一个硬币,更新dp[i],用dp[i-coin]来更新dp[i]
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
# 遍历所有coin 对于每一个coin 更新dp[i]
dp=[0]*(amount+1)
dp[0]=1
for coin in coins:
for i in range(coin,amount+1):
dp[i]+=dp[i-coin]
return dp[amount]
思路dp[i]+=dp[i-coin]想到了,但是误以为要遍历 0-amount,用dp[i-coin]更新dp[i],这样得到的结果会偏大,因为可能会有重复的组合。该思路计算的结果是排列数,而不是组合数,也就是代码会把1,2和2,1当做两种情况。但更加根本的原因是子问题定义出现了错误。
309. 最佳买卖股票时机含冷冻期
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if(len(prices)<=1):
return 0
# 0 持有
# 1 不持有,处于冷冻期
# 2 不持有,不处于冷冻期
dp=[[0]*3 for _ in range(0,len(prices))]
for i in range(0,len(prices)):
if(i==0):
dp[0][0]=-prices[0]
else:
dp[i][0]=max(dp[i-1][2]-prices[i],dp[i-1][0]) # 持有,①可以是买入;②也可以继续持有
dp[i][1]=dp[i-1][0]+prices[i] # 处于冷冻期 只能前一天持有 现在卖出
dp[i][2]=max(dp[i-1][1],dp[i-1][2]) # ①前一天冷冻;②前一天不冷动
return max(dp[-1][1],dp[-1][2])
优化空间复杂度,因为dp[i][]只和dp[i-1][]相关
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if(len(prices)<=1):
return 0
# s0 持有
# s1 不持有,处于冷冻期
# s2 不持有,不处于冷冻期
dp=[0]*len(prices)
s0=-prices[0]
s1=0
s2=0
for i in range(1,len(prices)):
s0_=max(s0,s2-prices[i])
s1_=s0+prices[i]
s2_=max(s1,s2)
s0,s1,s2=s0_,s1_,s2_
dp[i]=max(s1,s2)
return dp[i]
2021/3/3
139. 单词拆分
前[i+1]个字符子串判断时:
- 前0个字符 i+1字符
- 前1个字符 后i个字符
- …
leetcode比如判断leetco子串时:
- /leetco
- l/eetco
- le/etco
- lee/tco
- leet/co
- leetc/o
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
dp=[False]*(len(s)+1)
dp[0]=True # dp[i]表示前i个字符子串能否被dict拆分
# 前i个字符
# 前i个字符+1个字符
for i in range(1,len(s)+1):
for j in range(0,i):
if(dp[j] and s[j:i] in wordDict):
dp[i]=True
break
return dp[-1]
377. 组合总和 Ⅳ
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp=[0]*(target+1)
dp[0]=1
for i in range(1,target+1):
for num in nums:
if(num<=i):
dp[i]+=dp[i-num]
return dp[target]
714. 买卖股票的最佳时机含手续费
可以优化空间复杂度
class Solution:
def maxProfit(self, prices: List[int], fee: int) -> int:
# 0有股票
# 1无股票
if(len(prices)<=1):
return 0
# dp=[[0]*2 for i in range(0,len(prices))]
# dp[0][0]=-prices[0]-fee
# for i in range(1,len(prices)):
# # 有股票:(1)继续持有;(2)i-1无股票,i买入
# dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i]-fee)
# # 无股票;(1)基于无股票;(2)i-1有股票,i卖出
# dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i])
# return dp[-1][1]
no=0 # 无股票
yes=-prices[0]-fee # 有股票
for i in range(1,len(prices)):
# 无股票 两种情况
no_new=max(no,yes+prices[i])
# 有股票 两者情况
yes_new=max(yes,no-prices[i]-fee)
no,yes=no_new,yes_new
return no
2021/3/9
123. 买卖股票的最佳时机 III
注意两个地方:
- Q1:为什么buy2也用-prices[0]初始化
- Q2:一次循环的时候 buy1,sale1,buy2,sale2直接修改 不用临时变量存储
- 是因为可以看作当天买入 当天卖出 sale1=0 ;不太理解,是不是相当于 当prices[2]低于prices[1]时,在t=2时刻,buy1会更新为-prices[2],而sale1更新为0,(如果按照之前自己的想法,slae2=prices[2]-price[1]),他这种强制为0,并不是指一定要完成1次买入和卖出;还是不太理解。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if(len(prices)<2):
return 0
buy1=-prices[0]
sale1=0
buy2=-prices[0]
sale2=0
# Q1:为什么buy2也用-prices[0]初始化
# Q2:一次循环的时候 buy1,sale1,buy2,sale2直接修改 不用临时变量存储
for i in range(1,len(prices)):
if(prices[i]<-buy1):
buy1=-prices[i]
if(prices[i]+buy1>sale1):
sale1=prices[i]+buy1
if(sale1-prices[i]>buy2):
buy2=sale1-prices[i]
if(prices[i]+buy2>sale2):
sale2=prices[i]+buy2
return max(0,sale1,sale2)
官方给出:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
buy1 = buy2 = -prices[0]
sell1 = sell2 = 0
for i in range(1, n):
buy1 = max(buy1, -prices[i])
sell1 = max(sell1, buy1 + prices[i])
buy2 = max(buy2, sell1 - prices[i])
sell2 = max(sell2, buy2 + prices[i])
return sell2
583. 两个字符串的删除操作
和求最大子序列相同,想到思路即可,关键是判断尾字符,以及不同时取前两个的最大值
- 当两个尾字符相等时,
- 当两个尾字符不等时,
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
dp=[[0]*(len(word2)+1) for _ in range(0,len(word1)+1)]
for i in range(0,len(word1)):
for j in range(0,len(word2)):
if(word1[i]==word2[j]):
dp[i+1][j+1]=dp[i][j]+1
else:
dp[i+1][j+1]=max(dp[i+1][j],dp[i][j+1])
# print(dp)
return len(word1)+len(word2)-2*dp[len(word1)][len(word2)]
2021/3/12
72. 编辑距离
假设w[i]表示第i个单词
- w1[i]==w2[j]:不需要额外操作dp[i][j]=dp[i-1][j-1]
- w1[i]!=w2[j]:
- 删掉w1的i字符:dp[i-1][j] +删除
- 替换w1的i字符:dp[i-1][j-1]+替换
- 插入字符:只可能在i字符之后插入,dp[i][j-1]+插入
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
# dp[i][j] 表示w1前i个字符转化成w2前j个字符 最小操作数
len1,len2=len(word1),len(word2)
dp=[[0]*(len2+1) for _ in range(0,len1+1)]
# 初始化0行 0列
dp[0]=list(range(0,len2+1))
for i in range(1,len1+1):
dp[i][0]=i
# 开始循环
for i in range(1,len1+1):
for j in range(1,len2+1):
# S1:w1[i]=w2[j]
if(word1[i-1]==word2[j-1]):
# 不需增加操作
dp[i][j]=dp[i-1][j-1]
# S2: w1[i]!=w2[j]
else:
# 选出 删除、替换、插入的最小值
dp[i][j]=min(dp[i-1][j]+1,dp[i-1][j-1]+1,dp[i][j-1]+1)
#print(dp)
return dp[len1][len2]
2021/4/10
真的是2个月没刷题了。。
650. 只有两个键的键盘
dp[i][j]表示 target=i,最后由j作为被粘贴的数 得到的操作数
获得当前dp[i][j]后,两种情况
- s1:继续粘贴上一次的num;得到i+j
- s2:复制当前的数并且粘贴;得到2*i
但是代码速度很慢,是因为一个数能整除2的情况下,一定由复制得到的操作数小于复制1得到情况;
自己的代码相当于把所有情况弄出来了,但实际上只需要考虑最有效的路径
class Solution:
def minSteps(self, n: int) -> int:
if(n==1):
return 0
import sys
# 想法:dp[i][j]表示 target=i,最后由j作为被粘贴的数 得到的操作数
# 实际:dp[i][j] target=i+1 粘贴的数是j+1 所以dp的size可以加一行一列
#dp=[[sys.maxsize]*(n//2+1) for _ in range(n+1)]
dp=[[float('inf')]*(n//2+1) for _ in range(n+1)]
dp[1][1]=1
for i in range(1,n):
for j in range(1,n//2+1):
# s1:继续粘贴上一次的num
if(i+j<=n):
dp[i+j][j]=min(dp[i+j][j],dp[i][j]+1)
# s2:复制当前的数并且粘贴
if(2*i<=n):
dp[2*i][i]=min(dp[2*i][i],dp[i][j]+2)
#print(dp)
return min(dp[n])
官方题解,转化成数学题。。。。。
2021/7/23
97. 交错字符串
这种字符串的dp,很可能就是s1 s2分别做行列,然后可以使用滚动数组降低空间复杂度
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
n=len(s1)
m=len(s2)
if(n+m!=len(s3)):
return False
# dp[i][j] s1前i个 和 s2前j个 能否构成s3 前i+j个
dp=[False]*(m+1)
dp[0]=True
# dp[i][j]取决于 dp[i-1][j] s[j]和s1[i]
# dp[i][j] 表示s1前i个 所以是i+1
for j in range(1,m+1):
dp[j]=dp[j-1] and s2[j-1]==s3[j-1]
for i in range(1,n+1):
# 注意滚动数组:不能忘记只使用s1的情况
dp[0]=dp[0] and s1[i-1]==s3[i-1]
for j in range(1,m+1):
dp[j]=(dp[j] and s1[i-1]==s3[i+j-1]) or (dp[j-1] and s2[j-1]==s3[i+j-1])
return dp[m]
115. 不同的子序列
dp[i][j]表示【s前i个元素】中有【t前j个元素】的【子串数量】,这题状态更新还是容易乱,注意的点:
- 关于二维dp矩阵,为了统一,就直接弄成m+1行,n+1列(可以弄成m行 但是s和t的下标就不统一写法了);默认dp[0][0]=1
- 更新dp[i][\j]时:
- s[i-1]!=t[j-1]时(m+1,n+1这里下标就统一了),dp[i][j]=dp[i-1][j] (这个很好理解s变长了,并且新增的元素不等于t[j-1])
- s[i-1]t[j-1]时,说明s新增的元素等于t[j-1],即子串的最后一个字符,那么又分成两种情况:①还是不用这个新增的元素s[i-1],还是原来的结果dp[\i-1][j] ②用这个新增的元素作为字串的最后一个元素,结果就是用dp[i]中含字串[:-1]的结果来匹配上当前新增元素==;将①②结果相加即为结果
- 改成滚动数组,降低空间复杂度
class Solution:
def numDistinct(self, s: str, t: str) -> int:
m=len(s)
n=len(t)
if(n>m or n==0):
return 0
# dp[i][j]表示【s前i个元素】中有【t前j个元素】的【子串数量】
# 默认dp[0][0]=1
# dp[i][j]=dp[i-1][j] if s[i-1]!=t[j-1] else dp[i-1][j]+dp[i-1][j-1]
dp=[0]*(n+1) # 补0是为了dp[j-1]
dp[0]=1 # 补0是为了不用+1
for i in range(1,m+1):
dp_new=[0]*(n+1)
dp_new[0]=1
for j in range(1,n+1):
dp_new[j]=dp[j] if s[i-1]!=t[j-1] else dp[j]+dp[j-1]
dp=dp_new
return dp[n]
2021/9/13
132. 分割回文串 II
分为两步:
- 1.计算所有的可能回文idx dp[i][j]
- 2.使用cut[k]表示前k+1个字符串分割最小的次数 然后新增s[k]时,需要考虑0,1,2,…k-1的情况
时间复杂度 O(n^2)
class Solution:
def minCut(self, s: str) -> int:
# dp[i][j]表示 s[i:j+1]是否回文
# cut[k] 表示s[0:k+1]最少的次数
dp=[[True]*len(s) for _ in range(len(s))]
cut=[len(s)]*len(s)
cut[0]=0
# 回文dp[i][j]=(s[i]==s[j]) dp[i+1][j-1]
for i in range(len(s)-1,-1,-1):
for j in range(i+1,len(s)):
dp[i][j]=(s[i]==s[j]) and dp[i+1][j-1]
# 新增s[i]对应cut[i]
for i in range(1,len(s)):
# s[i]可以和s[i-1] s[i-2]..s[0]比较
# 0 1 idx ... i
if(dp[0][i]):
cut[i]=0
else:
for idx in range(1,i+1):
cut[i]=min(cut[i],cut[idx-1]+1) if dp[idx][i] else cut[i]
# print(dp)
# print(cut)
return cut[-1]
2021/9/14
1035. 不相交的线
这题基本和1143题一样
class Solution:
def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
# dp[i][j]表示num1前i个和num2前j个 最多的连线
# dp[i-1]==dp[j-1] dp[i][j]=dp[i-1][j-1]+1
# dp[i-1]!=dp[j-1] dp[i][j]=max(dp[i-1][j],dp[i][j-1])
m,n=len(nums1),len(nums2)
dp=[0]*(n+1)
for i in range(1,m+1):
dp_new=[0]*(n+1)
for j in range(1,n+1):
if(nums2[j-1]==nums1[i-1]):
dp_new[j]=dp[j-1]+1
else:
dp_new[j]=max(dp[j],dp_new[j-1])
dp=dp_new
return dp[-1]