一文解决动态规划
动态规划
详细总结所有动态规划面试题
跟着刷完你就会!
动态规划思路:
采用动态五部曲
1. 一维动态规划–简单dp:
斐波那契数
- dp数组的含义,dp[i]=F(i)的值
- 递推方程 dp[i]=dp[i-1]+dp[i-2]
- dp的初始化,dp[0]=0,dp[1]=1
- 从前到后遍历
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]
爬楼梯
- dp[i]表示到i阶梯有多少种爬楼梯方法
- dp[i-1]再跨一步或者dp[i-2]再跨两步就可以到dp[i]=dp[i-1]+dp[i-2]
- 初始化,dp[0]没有意义,dp[1]=1,dp[2]=2
- 从前到后遍历
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]
同样可以用两个变量降低一下空间复杂度
使用最小花费爬楼梯
- 到达第下标为i的台阶花费的最少体力dp[i]
- dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
- 初始化,dp[0]=cost[0],dp[1]=cost[1]
- 从前往后遍历,原数组:[1,100,1,1,1,100,1,1,100,1] dp数组:[1, 100, 2, 3, 3, 103, 4, 5, 104, 6]
- 因此最后要取dp[-1]和dp[-2]的最小值,因为这两个最后一步可以直达并且不花费
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)终点
- dp数组的含义是表示走到dp[i][j]位置的路径数量
- dp[i][j] = dp[i - 1][j] + dp[i][j - 1],只能从这两个方向过来
- dp[i][0]和dp[0][j]都是1,初始化,因为到这的路径只有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]
不同路径-有障碍物
- dp数组的含义依然是表示走到当前位置的路径dp[i][j]
- 但是如果位置不是障碍,才去推导dp[i][j]=dp[i][j-1]+dp[i-1][j], else=0
- 初始化和上面一样,但是要排除有障碍的情况,有障碍的后面就不初始化,因为无法到达,需要上行
- 遍历依然从上到下,从左到右
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]
整数拆分
- dp表示的的含义:拆分i,dp[i]表示拆分出的最大值
- dp[i]=max(dp[i],j*(i-j),j*dp[i-j]),j从1开始遍历,到j-1,要比较dp[i]的原因是dp[i]是循环保存的之前最大值
- 初始化dp[2]=1,dp[1]=0
- 遍历,i从3开始,j从1开始,正好用dp[2]的值,也用到了dp[1]的值但是并没有用,因为是0
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]
不同的二叉搜索树
- 分析dp[i]表示的是i个节点组成的二叉搜索树的种类
- dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量,j从1开始遍历,遍历到i
- 如上图的递推关系,我们发现以j为头节点,j-1左子树,i-j个右子树
- dp[0]=1,即左子树为空
- 遍历,i从1-n,j从1-i
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]
2. 复杂动态规划–0-1背包问题:
0-1背包问题:
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
- 确定dp数组以及下标的含义
对于背包问题,有一种写法, 是使用二维数组,即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][0]=0,没有疑问,因为背包容量为0的时候就没有任何价值
在看其他情况。
状态转移方程 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)
压缩0-1背包问题:
dp[i][j]只与dp[i-1]这一层的j有关,因此可以节省空间
- 确定dp数组的定义
在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。用循环表示拿到哪件商品了
- 递推,等于把i层的东西去掉了,递推方程为
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
- 初始化,因为dp[j]表示容量为j的背包,因此dp[0]=0,其余位置也初始化为0
- 二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
为什么呢?
倒序遍历是为了保证物品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)
分割等和子集:
- 这里首先将其套到背包问题,背包容量为sum/2,weight=nums,value=nums
- 即dp[j]表示容量为j的背包,当前放入i件物品之后容纳的最大价值
- 递推方程:dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]),这里的i表示拿到了第几个数
- 初始化,dp[0]=0,一个数字不放
- 遍历顺序:i从0->len(nums),j从sum/2->0
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
最后一块石头重量:
- 抽象到01背包问题,可以理解成,分出重量最多的两堆石头,即分出最高为一半重量的石头堆
- dp[j]表示重量为j的背包,在当前i之前放入后所能容纳的最大价值
- 递推方程:dp[j]=max(dp[j],dp[j-stones[i]]+stones[i])
- 初始化dp[0]=0,长度为weight/2
- 遍历顺序i从0->len(stones),j从weight/2-0
- 注意,这里我们先向下取整,得到左边这堆的最大重量dp[-1],右边的是sum-dp[-1]
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]
目标和:
- dp[j]表示当前i层所凑够背包j的解法数量,设➕号的和为x,剩下的数字放-号,为sum-x
- 注意,因为变成了求解法数量了,不是求当前背包j的最大价值了
- dp[j]+=dp[j-nums[i]]
- 初始化dp[0]=1,背包为0的解法是1种,即什么也不拿
- x-(sum-x)=target,x=(target+sum)/2,如果向下取整,奇数无解,排除,abs(S)>sum也无解,因此背包容量为x=(target+sum)/2
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]
一和零:
- 这题本质上是一个两个维度的0-1背包问题,mn表示01的容量限制,子集个数是价值
- dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]
- dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
- 初始化,背包容量为m,n,dp[0][0]=0
- 外层物品(字符串),内层容量(分别遍历mn,倒序)遍历。
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]
3. 复杂动态规划–完全背包问题:
完全背包问题:
有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)
最少的硬币数目:
- 经典的完全背包问题,每个硬币可以用无限次,背包容量为amount,价值为次数,求最少,dp[j]表示组成j金额的最少硬币数量
- 递推公式,dp[j]=min(dp[j],dp[j-coins[i]]+1])
- 初始化,dp[0]=0,dp=[amount+1]*(amount+1)
- 遍历外圈物品,内圈价值,正序从coins遍历。(完全背包正向遍历)
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]
硬币组合数目:
组合排列的数目。
组合和排列并不相同。
- dp的含义,dp[j]表示面值为j的组合数目
- 递推公式 dp[j] += dp[j - coins[i]];
- 初始化,dp[0]=1,其余的位置初始化位0就好
- 这里的遍历方式直接影响最后的结果。因为是无限背包问题,所以需要正序
如果是组合问题:应该先物品,后背包
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]
组合总和:
- dp[j]: 凑成目标正整数为j的排列个数为dp[j]
- dp[j] += dp[j - nums[i]];
- 初始化,dp[0]=1,其余初始化0
- 排列问题,所以外层遍历背包,内层遍历数字
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为跳的台阶层数
- dp[j]表示跳的阶梯是j的跳法
- dp[j]+=dp[j-nums[i]]
- 初始化,dp[0]=1,其余为0
- 遍历顺序,因为是排列,先上1和先上2是不一样的,所以先背包放外面,物品放内层
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
- dp[j]表示组成j的最少数量
- dp[j]=min(dp[j],dp[j-i*i]+1)
- dp初始化,dp[0]=0,其余的要初始化为n,即为n个1
- 遍历,先背包还是先物品都可以,然后完全背包问题需要正向
- i从0->ii<n,j从ii到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,是否可以装下。很浓的完全背包问题,因为字典中的词可以重复使用
- dp[j]表示0-j部分的字符串是否可以被字典组成,背包同样要len(s)+1,因为【0:len(s)】能拿到全部字符串
- dp[j]=dp[j-i]&[i:j] in wordict
- 初始化,dp[0]=1,单纯为了递推
- 遍历,先物品后背包都可以,物品0-len(s),背包i-len(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]
4. 普通动态规划-股票问题
打家劫舍:
- dp表示偷完i以内的房间赚到的最大价值
- dp[i]=max(dp[i-2]+nums[i],dp[i-1])
- 初始化,dp[0]=nums[0],dp[1]=max(nums[0],nums[1])
- 遍历,从0-n即可
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 -dp[i][0] 表示第i天持有股票所得最多现⾦,dp[i][1] 表示第i天不持有股票所得最多现⾦
-
- 这里我们要搞清楚,对现金来说,我们要持有股票,但现金要更多,不持有股票的话,卖出现金要更多。
- 对于持有股票来说,因为只能买一次,所以是现在买还是之前就买,是-prices[i],和dp[i-1][0]的最大值,不持有的话,是早卖dp[i-1][1]和现在卖prices[i]+dp[i-1][0]的最大值。
初始化,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]
买卖股票的最佳时机-k次购买
本题将上题扩展了一下,用二维数组表示dp
- dp[i][j],j从0-2*k,j奇数表示买入,偶数表示卖出
- dp递推,奇数dp[i][j]=dp[i-1][j-1]-prices[i]
- 偶数,dp[i][j]=dp[i-1][j-1]+prices[i]
- 初始化,dp[0][奇数]=-prices[0]
- 遍历顺序,从前到后
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]表示不持有股票的状态,dp[i][1]表示持有股票的状态
- 第i天持有股票:
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)
- 初始化,dp[0][0]=-prices[0],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],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])
5. 复杂动态规划-序列问题
最长上升子序列(不连续)
- dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度
- 位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值
- if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1)
- 每一个i,对应的dp[i](即最长上升子序列)起始大小至少都是1
- 因此j要从0-i-1,i要从1-n
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
最长连续递增子序列
- dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]。
- 如果 nums[i + 1] > nums[i],dp[i]=dp[i-1]+1
- 初始化,都为1
- 遍历顺序,一层循环
- 取dp最大值
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)
最长重复子数组(连续子序列)
- dp[i][j]表示两个数组以i-1和j-1结尾的数组中公共子数组最长的长度
- if nums1[i-1]==nums2[j-1]: dp[i][j]=dp[i-1][j-1]+1
- 初始化,都为0就好,长度要为n+1
- 遍历顺序,从1-n,1-m
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
最长公共子串(不连续)
- dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
- if text1[i-1]==text2[j-1]: dp[i][j]=dp[i-1][j-1]+1,else:dp[i][j]=max(dp[i][j-1],dp[i-1][j])
- 初始化都为0
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数组(dp table)以及下标的含义
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)