详细总结所有动态规划面试题
跟着刷完你就会!
采用动态五部曲
def fib(self, n):
dp=[0,1]
if n<2:
return dp[n]
for i in range(2,n+1):
dp.append(dp[i-1]+dp[i-2])
return dp[-1]
我们发现只用到前两个状态的数值,可以只用两个变量存储, 空间复杂度为O(1)
def fib(self, n):
dp=[0,1]
if n<2:
return dp[n]
for i in range(2,n+1):
tmp=dp[0]+dp[1]
dp[0]=dp[1]
dp[1]=tmp
return dp[-1]
def climbStairs(self, n):
dp=[0,1,2]
if n<3:
return dp[n]
for i in range(3,n+1):
dp.append(dp[i-1]+dp[i-2])
return dp[-1]
同样可以用两个变量降低一下空间复杂度
def minCostClimbingStairs(self, cost):
dp=[cost[0],cost[1]]
n=len(cost)
for i in range(2,n):
dp.append(min(dp[i-1],dp[i-2])+cost[i])
return min(dp[-1],dp[-2])
def minCost(self, costs: List[List[int]]) -> int:
#dp[i][j],j取0-2,表示第i间房子粉刷成三种颜色的最小花费
#初始化,dp[0][0-2]=costs[0][0-2]
#dp[i][0]=min(dp[i-1][1],dp[i-1][2])+costs[i][0]
#滚动数组覆盖
dp=costs[0].copy()
for i in range(1,len(costs)):
tmp=dp.copy()
dp[0]=min(tmp[1],tmp[2])+costs[i][0]
dp[1]=min(tmp[0],tmp[2])+costs[i][1]
dp[2]=min(tmp[0],tmp[1])+costs[i][2]
return min(dp)
dp[i][0]=dp[i-1][0]+(s[i]==0? 0:1)
dp[i][1]=min(dp[i-1][0]+dp[i-1][1])+(s[i]==1?0:1)
dp[0]=[(s[0]==0?0:1),(s[1]==1?0:1)]
def minFlipsMonoIncr(self, s: str) -> int:
dp=[0 if s[0]=="0" else 1,0 if s[0]=="1" else 1]
for i in range(1,len(s)):
tmp=dp.copy()
dp[0]=tmp[0]+(0 if s[i]=="0" else 1)
dp[1]=min(tmp[0],tmp[1])+(0 if s[i]=="1" else 1)
return min(dp)
机器⼈从(0 , 0) 位置触发,到(m - 1, n - 1)终点
def uniquePaths(self, m, n):
dp=[[0]*n for _ in range(m)]
for i in range(m):
dp[i][0]=1
for i in range(n):
dp[0][i]=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[-1][-1]
def uniquePathsWithObstacles(self, obstacleGrid):
m=len(obstacleGrid)
n=len(obstacleGrid[0])
dp=[[0]*n for _ in range(m)]
for i in range(m):
if obstacleGrid[i][0]==1:
break
dp[i][0]=1
for i in range(n):
if obstacleGrid[0][i]==1:
break
dp[0][i]=1
for i in range(1,m):
for j in range(1,n):
if obstacleGrid[i][j]==0:
dp[i][j]=dp[i-1][j]+dp[i][j-1]
else:
dp[i][j]=0
return dp[-1][-1]
def integerBreak(self, n):
dp=[0]*(n+1)
dp[2]=1
for i in range(3,n+1):
for j in range(1,i):
dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]))
return dp[-1]
def numTrees(self, n):
dp=[0]*(n+1)
dp[0]=1
for i in range(1,n+1):
for j in range(1,i+1):
dp[i]+=dp[j-1]*dp[i-j]
return dp[-1]
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
那么可以有两个方向推出来dp[i][j],
**不放物品i:**由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
**放物品i:**由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
在看其他情况。
状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
#初始化
weight=[1,3,4]
value=[15,20,30]
w=len(weight)
n=4
dp=[[0]*(n+1) for _ in range(w)]
for j in range(n+1):
if j<weight[0]:
dp[0][j]=0
else:
dp[0][j]=value[0]
先遍历物品,后遍历背包重量
物品i从1- >w,j->bagweight
for i in range(1,w):
for j in range(0,n+1):
if j<weight[i]:
dp[i][j]=dp[i-1][j]
else:
dp[i][j]=max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
print(dp)
dp[i][j]只与dp[i-1]这一层的j有关,因此可以节省空间
在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。用循环表示拿到哪件商品了
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
为什么呢?
倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么前面的物品0就会被重复加入多次!在max的前提下
weight=[1,3,4]
value=[15,20,30]
w=len(weight)
n=4
dp=[0]*(n+1)
for i in range(0,w):
for j in range(n,weight[i]-1,-1): #必须倒序,才能用到上一层的前面的值,不然会反复加
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
print(dp)
def canPartition(self, nums):
bag_sum=sum(nums)
if bag_sum%2==1:
return False
bag_sum=bag_sum/2
dp=[0]*(bag_sum+1)
for i in range(len(nums)):
for j in range(bag_sum,-1,-1):
if j>=nums[i]:
dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
return True if dp[bag_sum]==bag_sum else False
def lastStoneWeightII(self, stones):
bag_sum=sum(stones)
target=bag_sum//2
dp=[0]*(target+1)
for i in range(len(stones)):
for j in range(target,-1,-1):
if j>=stones[i]:
dp[j]=max(dp[j],dp[j-stones[i]]+stones[i])
return bag_sum-2*dp[-1]
def findTargetSumWays(self, nums, target):
num_sum=sum(nums)
if (target+num_sum)%2==1 or abs(target)>num_sum:
return 0
max_bag=(target+num_sum)//2
dp=[0]*(max_bag+1)
dp[0]=1
for i in range(len(nums)):
for j in range(max_bag,nums[i] - 1,-1):#这里必须要-1,到0,因为dp[0]!=0
dp[j]+=dp[j-nums[i]]
return dp[-1]
def findMaxForm(self, strs, m, n):
dp=[[0]*(n+1) for _ in range(m+1)]
for one_str in strs:
oneNum = 0
zeroNum = 0
for s in one_str:
if s=='0':
zeroNum+=1
else:
oneNum+=1
for i in range(m,zeroNum-1,-1):
for j in range(n,oneNum-1,-1):
dp[i][j]=max(dp[i][j],dp[i - zeroNum][j - oneNum] + 1)
return dp[-1][-1]
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
唯一要改的就是遍历的顺序的一个设置,不用倒序遍历,而是要正序!
weight=[1,3,4]
value=[15,20,30]
w=len(weight)
n=4#最大容量
dp=[0]*(n+1)
for i in range(0,w):
for j in range(weight[i],n+1): #正序
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
print(dp)
def coinChange(self, coins, amount):
dp=[amount+1]*(amount+1)
dp[0]=0
for i in range(len(coins)):
for j in range(coins[i],amount+1):
dp[j]=min(dp[j],dp[j-coins[i]]+1)
if dp[-1]==amount+1:#如果依然是初始化的值,说明没法凑成
return -1
else:
return dp[-1]
组合排列的数目。
组合和排列并不相同。
如果是组合问题:应该先物品,后背包
for i in range(len(coins)):
for j in range(coins[i],amount+1):
dp[j]+=dp[j-coins[i]]
如果是排列问题,应该先背包后物品。
for j in range(1,amount+1):
for i in range(len(coins)):
if j >=coins[i]:
dp[j]+=dp[j-coins[i]]
因此本题代码:
def change(self, amount, coins):
dp=[0]*(amount+1)
dp[0]=1
for i in range(len(coins)):
for j in range(coins[i],amount+1):
dp[j]+=dp[j-coins[i]]
return dp[-1]
def combinationSum4(self, nums, target):
dp=[0]*(target+1)
dp[0]=1
for j in range(1,target+1):
for i in range(len(nums)):
if j>=nums[i]:
dp[j]+=dp[j-nums[i]]
return dp[-1]
跳的阶数就是物品,楼顶就是amount,跳法就是组合数目,这里我们设置nums为跳的台阶层数
def climbStairs(self, n):
dp=[0]*(n+1)
dp[0]=1
nums=[1,2]
for j in range(1,n+1):
for i in range(len(nums)):
if j>=nums[i]:
dp[j]+=dp[j-nums[i]]
return dp[-1]
首先分析,很浓的完全背包问题,因此需要正向遍历,然后是求最小价值,因此递推用min并且不用区分组合还是排列。即先背包还是先物品都可以
背包容量为n,物品为1,4,9.。。。一直到小于n
def numSquares(self, n):
dp=[n]*(n+1)
dp[0]=0
thinglen=int(pow(n,0.5))
for i in range(thinglen+1):
for j in range(i*i,n+1):
dp[j]=min(dp[j],dp[j-i*i]+1)
return dp[-1]
抽象成背包问题,首先物品是字典中的字符串,背包是s,是否可以装下。很浓的完全背包问题,因为字典中的词可以重复使用
def wordBreak(self, s, wordDict):
dp=[False]*(len(s)+1)
dp[0]=True
for i in range(len(s)):
for j in range(i+1,len(s)+1):
if dp[i] and s[i:j] in wordDict:
dp[j]=True
return dp[-1]
def wordBreak(self, s, wordDict):
dp=[False]*(len(s)+1)
dp[0]=True
for j in range(1,len(s)+1):
for i in range(0,j):
if dp[i] and s[i:j] in wordDict:
dp[j]=True
return dp[-1]
def rob(self, nums):
if len(nums)==0:
return 0
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-2]+nums[i],dp[i-1])
return dp[-1]
唯一的区别就是最后一个房子和第一个房子相连,不能同时偷
将偷的代码封装成一个函数
分别考虑头到尾-1,和头+1到尾的两种情况,取最大。
def rob(self, nums):
def robv1(nums,l,r):
if r==l:
return nums[l]
dp=[0]*(r-l+1)
dp[0]=nums[l]
dp[1]=max(nums[l],nums[l+1])
for i in range(2,r-l+1):
dp[i]=max(dp[i-2]+nums[l+i],dp[i-1])
return dp[-1]
if len(nums)==0:
return 0
if len(nums)==1:
return nums[0]
result1=robv1(nums,0,len(nums)-2)
result2=robv1(nums,1,len(nums)-1)
return max(result1,result2)
首先本题要用后序遍历
在树上构建dp数组,记录每个节点偷或者不偷的val
然后最终取val在根节点的val
dp递推
不偷当前节点left=max(result1[0],result1[1])+max(result2[0],result2[1])
偷当前节点right=node.val+result1[0]+result2[0]
[left,right]
def rob(self, root):
def robtree(node):
if not node:
return [0,0]
result1=robtree(node.left)
result2=robtree(node.right)
left=max(result1[0],result1[1])+max(result2[0],result2[1])
right=node.val+result1[0]+result2[0]
return [left,right]
result=robtree(root)
return max(result[0],result[1])
贪心解法:
if not prices:
return 0
profit=0
cost=prices[0]
for i in range(1,len(prices)):
cost=min(cost,prices[i])
profit=max(profit,prices[i]-cost)
return profit
初始化,dp[0][0]=-prices,dp[0][1]=0,刚买就卖
def maxProfit(self, prices):
if not prices:
return 0
dp=[[0,0] for _ in range (len(prices))]
dp[0][0]=-prices[0]
dp[0][1]=0
for i in range(1,len(prices)):
dp[i][0]=max(dp[i-1][0],-prices[i])
dp[i][1]=max(dp[i-1][1],prices[i]+dp[i-1][0])
return dp[-1][1]
贪心解法:
def maxProfit(self, prices):
#等价于每天都在买卖
profit = 0
for i in range(1, len(prices)):
tmp = prices[i] - prices[i - 1]
if tmp > 0:
profit += tmp
return profit
和上面的题的区别在于,
现金,要么不买,是dp[i-1][0],
要么用dp[i-1][1]-prices[i],用已有不持股的利润去买股票
dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i])
不吃股票得到的最多利润不变
def maxProfit(self, prices):
if not prices:
return 0
dp=[[0,0] for _ in range (len(prices))]
dp[0][0]=-prices[0]
dp[0][1]=0
for i in range(1,len(prices)):
dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i])
dp[i][1]=max(dp[i-1][1],prices[i]+dp[i-1][0])
return dp[-1][1]
初始化:dp[0][1]=-prices[0],dp[0][2]=0
遍历顺序和刚才相同:
def maxProfit(self, prices):
if not prices:
return 0
dp=[[0,0,0,0,0] for _ in range (len(prices))]
dp[0][1]=-prices[0]
dp[0][3]=-prices[0]
for i in range(1,len(prices)):
dp[i][0]=dp[i-1][0]
dp[i][1]=max(dp[i-1][1],-prices[i]+dp[i-1][0])
dp[i][2]=max(dp[i-1][2],prices[i]+dp[i-1][1])
dp[i][3]=max(dp[i-1][3],-prices[i]+dp[i-1][2])
dp[i][4]=max(dp[i-1][4],prices[i]+dp[i-1][3])
return dp[-1][4]
本题将上题扩展了一下,用二维数组表示dp
def maxProfit(self, k, prices):
if not prices:
return 0
dp=[[0]*(2*k+1) for _ in range (len(prices))]
for i in range(2*k+1):
if i%2==1:
dp[0][i]=-prices[0]
for i in range(1,len(prices)):
dp[i][0]=dp[i-1][0]
for j in range(1,2*k+1):
if j%2==1:
dp[i][j]=max(dp[i-1][j],-prices[i]+dp[i-1][j-1])
else:
dp[i][j]=max(dp[i-1][j],prices[i]+dp[i-1][j-1])
return dp[-1][-1]
贪心算法
def maxProfit(self, prices, fee):
#贪心
#等价于每天都在买卖
profit = 0
buy=prices[0]+fee
for i in range(1, len(prices)):
if prices[i]+fee<buy:
buy=prices[i]+fee
elif prices[i]-buy>0:
profit += prices[i]-buy
buy=prices[i]#这里是因为并不知道现在卖了会是最大收益,因此保存buy,之后循环如果prices直接大于当前buy,那么等于使用后面的价格卖的,pmax-pmin+profit=用最高价卖的
return profit
我们还是来分析dp,可以多次卖,那么使用dp[i][0-1]来表示持有和持有不持有股票的最大收益
dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i])
第i天卖出股票
dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]-fee)
def maxProfit(self, prices):
if not prices:
return 0
dp=[[0,0] for _ in range (len(prices))]
dp[0][0]=-prices[0]
dp[0][1]=0
for i in range(1,len(prices)):
dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i])
dp[i][1]=max(dp[i-1][1],prices[i]+dp[i-1][0]-fee)
return dp[-1][1]
dp[i][0]=max(dp[i-1][0],max(dp[i-1][1],dp[i-1][3])-prices[i])
dp[i][1]=max(dp[i-1][1],dp[i-1][3])
dp[i][2]=dp[i-1][0]+prices[i]
dp[i][3]=dp[i-1][2]
综上
def maxProfit(self, prices):
if not prices:
return 0
dp=[[0,0,0,0] for _ in range (len(prices))]
dp[0][0]=-prices[0]
for i in range(1,len(prices)):
dp[i][0]=max(dp[i-1][0],max(dp[i-1][1],dp[i-1][3])-prices[i])
dp[i][1]=max(dp[i-1][1],dp[i-1][3])
dp[i][2]=dp[i-1][0]+prices[i]
dp[i][3]=dp[i-1][2]
return max(dp[-1])
def lengthOfLIS(self, nums):
dp=[1]*(len(nums))
result=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)
result=max(result,dp[i])
return result
def findLengthOfLCIS(self, nums):
dp=[1]*(len(nums))
for i in range(1,len(nums)):
if nums[i]>nums[i-1]:
dp[i]=dp[i-1]+1
return max(dp)
def findLength(self, nums1, nums2):
dp=[[0]*(len(nums2)+1) for _ in range(len(nums1)+1)]
result=0
for i in range(1,len(nums1)+1):
for j in range(1,len(nums2)+1):
if nums1[i-1]==nums2[j-1]:
dp[i][j]=dp[i-1][j-1]+1
result=max(dp[i][j],result)
return result
def longestCommonSubsequence(self, text1, text2):
dp=[[0]*(len(text2)+1) for _ in range(len(text1)+1)]
for i in range(1,len(text1)+1):
for j in range(1,len(text2)+1):
if text1[i-1]==text2[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]
要求找到子串原串:
dp=[ [""]*(len(text2)+1) for _ in range(len(text1)+1)]
for i in range(1,len(text1)+1):
for j in range(1,len(text2)+1):
if text1[i-1]==text2[j-1]:
dp[i][j]=dp[i-1][j-1]+text2[j-1]
else:
if len(dp[i-1][j])>len(dp[i][j-1]):
dp[i][j]=dp[i-1][j]
else:
dp[i][j]=dp[i][j-1]
return dp[-1][-1]
其实就是求两个字符串的最长公共子序列的长度!不要求连续,但是必须保证相对顺序
def maxUncrossedLines(self, nums1, nums2):
dp=[[0]*(len(nums2)+1) for _ in range(len(nums1)+1)]
for i in range(1,len(nums1)+1):
for j in range(1,len(nums2)+1):
if nums1[i-1]==nums2[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]
dp[i]表示以i结尾的的最大连续子序列最大和
dp[i]=max(nums[i],dp[i-1]+nums[i])
nums[i]从当前从头计算,因为要求连续
初始化,dp[0]=nums[0],长度等同nums
def maxSubArray(self, nums):
dp=[0]*len(nums)
dp[0]=nums[0]
res=nums[0]
for i in range(1,len(nums)):
dp[i]=max(nums[i],nums[i]+dp[i-1])
res=max(dp[i],res)
return res
双指针做法:
def isSubsequence(self, s, t):
l=0
r=0
while l<len(s) and r<len(t):
if s[l]==t[r]:
l+=1
r+=1
if l==len(s):
return True
else:
return False
dp做法:
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]
初始化就都初始化为0就好了。
def isSubsequence(self, s, t):
dp=[[0]*(len(t)+1) for _ in range(len(s)+1)]
for i in range(1,len(s)+1):
for j in range(1,len(t)+1):
if s[i-1]==t[j-1]:
dp[i][j]=dp[i-1][j-1]+1
else:
dp[i][j]=dp[i][j-1]
return dp[-1][-1]==len(s)