动态规划DP
746. 使用最小花费爬楼梯 & 牛客网
数组的每个索引做为一个阶梯,第 i个阶梯对应着一个非负数的体力花费值 cost[i](索引从0开始)。
每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。
您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。
示例 1:
输入: cost = [10, 15, 20]
输出: 15
解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。
示例 2:
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。
注意:
cost 的长度将会在 [2, 1000]。
每一个 cost[i] 将会是一个Integer类型,范围为 [0, 999]。
思路:动态规划,爬当前的楼梯 (第i阶)所花费的代价和第i-1阶与第i-2阶相关。
举例子: 令dp[i]表示跨过当前第i阶台阶需要的最小代价。
cost = [15, 3], 那么dp[0] = 15, dp[1] = 3, 最小的代价为 min(15,3) = 3
cost = [15, 3, 1], 那么dp[0] = 0, dp[1] = 3, dp[2] = 4, 最小的代价为min(3,4) = 3
所以可推出如下的通式:
Base case:
dp[0]=cost[0]dp[1]=cost[1]
DP formula:
dp[i]=cost[i]+min(dp[i-1],dp[i-2])
cost = list(map(int, input().split(','))) dp = [cost[0], cost[1]] for i in range(2, len(cost)): dp.append(cost[i]+min(dp[i-1], dp[i-2])) print(min(dp[-1], dp[-2]))
方格走法
有一个X*Y的网格,小团要在此网格上从左上角到右下角,只能走格点且只能向右或向下走。请设计一个算法,计算小团有多少种走法。给定两个正整数int x,int y,请返回小团的走法数目。
思路:动态规划:对于这样一个(x+1)乘(y+1)的网格点,用dp[i][j]表示走到i,j这个点时所拥有的走法数目。那么递推公式就是:
dp[i][j] = dp[i-1][j] + dp[i][j-1], 意思是在当前这个网格基础上,走法有两种:一种是从该点上面走过来的,另一种是从该点左边走过来的。所以在i,j点的走法是这两种走法的和。
输入:输入包括一行,空格隔开的两个正整数x和y,取值范围[1,10]。
输出:输出一行,表示走法的数目。
x,y = list(map(int, input().split()))
dp = [[1]*(x+1)] * (y+1)
for i in range(1,y+1):
for j in range(1,x+1):
dp[i][j] = dp[i-1][j]+dp[i][j-1]
print(dp[-1][-1])
例如N=17,1+8+8 = 17,最少需要3个立方数,则输出3。
N= 28,1+1+1+1+8+8+8=28, 需要7个立方数,1+27=28,需要2个立方数,所以最少立方数为2,则输出2。
输入描述:
一个数字N(0<N<1000000)
输出描述:
最少立方数个数
思路:先用无穷大填充至长度为N的列表,然后在立方数的位置上赋值为1,之后对于第i个位置上的dp[i+temp],temp为循环的立方数,dp[i+temp] = min(dp[i]+1, dp[i+temp])。
N = int(input())
inf = 1000000
dp = [inf]*(N+1)
k = 1
dp[0],dp[1] = 0,1
for i in range(1,N+1):
temp = 1
premin = inf
while(temp**3<=i):
premin = min(premin, dp[i-temp**3])
temp+=1
dp[i] = premin+1
print(dp[-1])
下面是leetcode (279. Perfect Squares)最小平方数和,和上题基本一样:
思路:
dp[0] = 0
dp[1] = dp[0]+1 = 1
dp[2] = dp[1]+1 = 2
dp[3] = dp[2]+1 = 3
dp[4] = Min{ dp[4-1*1]+1, dp[4-2*2]+1 }
= Min{ dp[3]+1, dp[0]+1 }
= 1
dp[5] = Min{ dp[5-1*1]+1, dp[5-2*2]+1 }
= Min{ dp[4]+1, dp[1]+1 }
= 2
.
.
.
dp[13] = Min{ dp[13-1*1]+1, dp[13-2*2]+1, dp[13-3*3]+1 }
= Min{ dp[12]+1, dp[9]+1, dp[4]+1 }
= 2
.
.
.
dp[n] = Min{ dp[n - i*i] + 1 }, n - i*i >=0 && i >= 1
class Solution:
def numSquares(self, n: int) -> int:
temp = [10000]*(n+1)
temp[0] = 0
for i in range(1, n+1):
min_t = 10000
j = 1
while(i>=j*j):
min_t = min(min_t, temp[i-j*j]+1)
j+=1
temp[i] = min_t
return temp[-1]
0-1 背包问题讲解:

首先我们需要直到状态转移方程。定义为F(n,C)为将n个物体放到容量为C的包中的价值,那么F(i,c)为将第i个物体放入后的最大价值,其实就等于以下两种情况的最大值:不将该物品放入、在有i-1个物品的基础上(此事容量为c-W(i)),放入该物品。即下图所示:

首先是递归解法:
class Solution: # 返回将n件物品放到大小为c的包中的最大价值
def knapsack01(self, w, v, n, c ): # w为体积列表,v为价值列表,n为物品数,c为总背包容积
if n<=0 or c<=0: # 如果需要放的物品数<=0或者背包容积<0,则最大价值为0
return 0
res = self.knapsack01(w,v,n-1,c) # 对应于情况1:当前物品没有放到C大小的包中
if c>=w[n-1]:
res = max(res, v[n-1]+self.knapsack01(w,v,n-1,c-w[n-1])) # 对应于情况2:在容积大小为c-w[n-1]的包中再放入该物品
return res
s = Solution()
print(s.knapsack01([2,3,4,5],[3,4,5,7],4,9))
其次是自底向上求解的方法:通过表格来说明计算过程:
首先表格数组Arr的每一行是代表一个物品,列数就是背包容量大小。Arr[0][0]=0,因为0个东西放到大小为0的包中。同理,由于第一件物品大小为1,所以背包容积只要大于等于1都可以将其放入,所以第一行其余元素(价值)就等于第一件物品的价值为6。下面考虑Arr[1][3],如下图价值是16,怎么得到的呢?首先当前物件id为1,我们不把它放到包里,那么此时价值为6,另一种情况是我们可以将其放入,得到这样的价值:10+Arr[0][3-2] = 10+Arr[0][1] = 10+6=16。取两种情况的最大值max(6, 16)=16。

根据以上的思考,我们可以补全这张表,思路就是先补全第一行,其余按照行中每个元素都考虑两种情况的最大值即可。那么Arrs[2][5]即为将3个物品放到容量为5的包的最大价值。

代码实现如下:
class Solution:
def knapsack01(self, w, v, c ):
if len(w)<=0 or c<=0: return 0
Arrs = [[0]*(c+1) for i in range(len(w))]
for i in range(c+1): # 针对第一行
Arrs[0][i] = v[0] if i>=w[0] else 0
for i in range(1,len(v)): # 针对其余行
for j in range(c+1):
Arrs[i][j] = Arrs[i-1][j] # 第一种情况
if j>=w[i]: # 第二种情况(注意判断语句不要丢)
Arrs[i][j] = max(Arrs[i][j],v[i]+Arrs[i-1][j-w[i]])
return Arrs[-1][-1]
可以看到这个解法的两种情况讨论和递归方法一毛一样。
再来一道背包的变种题目 : 416. Partition Equal Subset Sum
题意大概是这样:对于一个数组:[1, 5, 11, 5],是否可以均分为两部分。这个例子可以:11和1+5+5。这道题是背包的变种。等价于求解数组中的元素能否正好填满数组和的一半。代码如下:
class Solution: def canPartition(self, nums) -> bool: n = len(nums) c = sum(nums) if c==0 or c%2 ==1: return False c = c//2 dp = [[0]*(c+1) for i in range(n)] # dp[i][c]为i个物品是否能填满容量为c的包 for i in range(c+1): dp[0][i] = 1 if nums[0]==i else 0 for i in range(1,n): for j in range(c+1): dp[i][j] = dp[i-1][j] if j-nums[i]>=0: dp[i][j] = dp[i][j] or dp[i-1][j-nums[i]] # 这里是或操作 res = 0 for i in range(n): res += dp[i][-1] # 不论第几个物品吗,只要最后一列有1,即容量为c,则满足 return True if res else False s = Solution() print(s.canPartition([1,11,5,5]))
############### 0-1 背包优化 ##########
以上不论是自底向上还是记忆化搜索的时空复杂度都是O(n*C)。以上的解法都只需要记住一个通式:
F(n,C)表示考虑将n个物品放进容量为C的背包,使得价值最大。
![]()
其实我们发现,第i行元素都只依赖于第i-1行元素,理论上只需要保持2行元素即可。此时空间复杂度变为O(2*C)= O(C)。修改方法很简单,即只需要0-1两行,我们只需将原代码中的i行改为i%2即可。那有没有可能只用1行呢?可以的。回顾我们之前的做法,对于当前元素,我们取该元素正上方元素和左上方元素的最大值,所以我们可以在本行从右向左直接更新本行内容。
以下是利用优化后的方法解决两道牛客网上的背包问题:(虽然我感觉还是上面的方法方便记忆...)
M,N,cost,val = input().split(',')
M,N,cost,val = int(M),int(N),list(map(int,cost.split())),list(map(int,val.split()))
dp = [0]*(M+1) # 背包最大容积为M,N件物品,每个物品的体积存于cost,每个物品的价值存于val。
res = 0 # 按题意,dp[i]为容积为i的包所能装的最大价值
for i in range(N): # 按照物品数量循环
j = M
while(j-cost[i]>=0): # 背包容量由大到小,判断当前背包容量能否装进第i件物品
dp[j] = max(dp[j], dp[j-cost[i]]+val[i]) # 容量为j的包最大价值,应该去和容量为j-cost[i]的包+val[j]这个价值去比
j-=1 # 背包容量减1
res = max(res, dp[j]) # 更新最大价值
print(dp[-1])
上面的背包问题是在体积满足要求的情况下求解最大的价值。下面是非常类似的背包问题,求解是否存在正好等于背包体积的组合方式。
n = int(input()) # n个物体
arrs = list(map(int, input().split())) # n个物体的体积
k = int(input()) # 背包容积
dp = [0]*(k+1) # 按照背包容积大小初始化数组
dp[0] = 1 # 根据题意,背包大小为0时肯定满足,所以为1。按题意dp[i]代表容积为i的包是否能正好被装满,1为装满,否则为0
for i in range(n): # 按照物品数量循环
j = k
while(j-arrs[i]>=0): # 背包容量又大到小,判断当前背包能否装下第i件物品
dp[j] = max(dp[j], dp[j-arrs[i]]) # 容量为j的包能否被装满取决于容量为dp[j-arrs[i]]的包能否装满
j-=1 # 背包容量减1
print(dp[-1]) # 打印出容量为k的包的状态,即1为可被装满,否则为0
总结:
- 初始化dp数组大小为背包容积大小+1。dp[i]的意义根据题意决定。初始化dp[0]。
- 两层循环,第一层为按照各个物品,第二层按照背包大小,判断此时包的大小能否装下当前物品。
- 内循环dp[j] 取决于他自身和dp[j-arrs[i]]的关系。
背包问题的变种
494. 目标和
leetcode上面两道动态规划,每一个当前状态都取决于上一次的状态。
64. Minimum Path Sum

图源:慕课网
记忆化搜索(自顶向下)
leetcode:343. Integer Break
思路来源于暴力解法:

根据暴力解法可以看出,n-1, n-2,,,2,1等是被多次重复使用的。即要求n的最大乘积,即要找n-1的最大乘积*1, n-2的最大乘积*2 。。。。 所以可以先写出这样的代码:
class Solution:
def integerBreak(self, n: int) -> int:
if n==1: return 1
res = -1
for i in range(1, n):
res = max(res, i*(n-i), i*self.integerBreak(n-i))return res
注意上面 i*(n-i)指的是当前不做分解求得的最大值。上面的解法会超时,我们发现其实n-1, n-2,,,2,1等是被多次重复使用的,所以可以弄一个记录看看是否已经有n-i的分割结果了。
class Solution: def integerBreak(self, n: int) -> int: if n==1: return 1 temp = [-1]*(n+1) if temp[n]!=-1: return temp[n] res = -1 for i in range(1, n): res = max(res, i*(n-i), i*self.integerBreak(n-i)) temp[n] = res return res
然而此时还会超时。。 下面用动态规划的方法来解决。即首先计算n=1的情况,然后是n=2.。。。这与上面的自顶向下正好相反。
class Solution:
def integerBreak(self, n: int) -> int:
if n==1: return 1
temp = [-1]*(n+1)
temp[1] = 1
for i in range(2, n+1):
for j in range(1,i):
temp[i] = max(temp[i], j*(i-j), j*temp[i-j])
return temp[-1]
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
考虑如下记忆化搜索(自顶向下)

状态和状态转移方程如下:

f(0) 即为考虑偷取【0,n-1】范围内的房子。
最简单的写法可以如下: 自顶向下:即f(x)为x~n的结果。 自底向上:即f(x)为0~x的结果。
class Solution:
def rob(self, nums) -> int:
if len(nums)==0: return 0
if len(nums)==1: return nums[0]
res = 0
for i in range(len(nums)):
res = max(res, nums[i]+self.rob[i+2:])
return res
然而和上题一样,会有许多重复的计算,所以我们弄一个记忆化搜索来记录已经得到的值:但是这里没有索引,所以可以自定义一个函数来处理。下面是动态规划,自底向上。
动态规划状态的定义:f(x) 即为考虑偷取【0,x】范围内的房子。
class Solution:
def rob(self, nums) -> int:
if len(nums)==0: return 0
temp = [0]*(len(nums)+1)
temp[0] = 0 # temp[i]表示nums[0到i]房屋能抢劫到的最大值
temp[1] = nums[0]
for i in range(2, len(nums)+1):
temp[i] = max(temp[i-1], nums[i-1]+temp[i-2]) # 注意nums的下标要减1,因为我们将temp长度增加了1
return temp[-1]
如果将上题改为首位相连的房子,即首尾房子不能被同时打劫(213. 打家劫舍 II),那么应该将首尾分别剔除,做普通的打劫,再比较最大值。
class Solution: def rob(self, nums) -> int: def rob_one(nums) -> int: temp = [0]*(len(nums)+1) temp[0] = 0 # temp[i]表示nums[0到i]房屋能抢劫到的最大值 temp[1] = nums[0] for i in range(2, len(nums)+1): temp[i] = max(temp[i-1], nums[i-1]+temp[i-2]) return temp[-1] if len(nums)==0: return 0 if len(nums)==1: return nums[0] return max(rob_one(nums[1:]), rob_one(nums[:-1]))
91. Decode Ways 解码方式
一条包含字母 A-Z 的消息通过以下方式进行了编码:
'A' -> 1
'B' -> 2
...
'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
示例 1:
输入: "12"
输出: 2
解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。
动态规划思路: 假设字符串‘123’, 那我们开辟一个长度为n的数组dp[n], dp[i] 表示dp[0:i]的字符串的编码总数。那么123这个长度为3的编码方式 = 长度为1的编码(1,23)和长度为2的编码(12,3)的方式的和。所以通式为 dp[n] = dp[n-1]+dp[n-2]。当然实际实现是有一些限制的。例如n-2要满足n>1。以及要考虑当前字母是0的话,那么dp[n] = dp[n-2]
class Solution:
def numDecodings(self, s: str) -> int:
if not s: return 0
dp = [0]*(len(s)+1)
dp[0] = 1
for i in range(1, len(s)+1):
if s[i-1]!='0': # 如果当前字母不为0,那么可以考虑i-1的情况, 为0则不能考虑。例如 120不能考虑(12,0),只能考虑(1,20)
dp[i]+=dp[i-1]
if i>1 and s[i-2:i]<'27' and s[i-2:i]>'09': # 考虑i-2的情况,要注意此时的数在不在09-27之内。 例如(12,09)不行,(12,28)不行
dp[i]+=dp[i-2]
return dp[-1]
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
if m<=0 or n<=0: return 0
dp = [[1]*n for i in range(m)]
for i in range(m):
for j in range(n):
if i-1>=0 and j-1<0:
dp[i][j]=dp[i-1][j]
elif j-1>=0 and i-1<0:
dp[i][j]=dp[i][j-1]
elif i-1<0 and j-1<0:
dp[i][j] = 1
else:
dp[i][j] = dp[i-1][j]+dp[i][j-1]
return dp[-1][-1]
带有障碍物的路径 63. Unique Paths II:
class Solution: def uniquePathsWithObstacles(self, obstacleGrid) -> int: m = len(obstacleGrid) n = len(obstacleGrid[0]) dp = [[0]*n for i in range(m)] for i in range(m): for j in range(n): if obstacleGrid[i][j]==1: dp[i][j] = 0 elif i==0 and j==0: dp[i][j] = 1 else: if i==0:dp[i][j] = dp[i][j-1] elif j==0:dp[i][j] = dp[i-1][j] else: dp[i][j] = dp[i-1][j]+dp[i][j-1] return dp[-1][-1]
买卖股票问题
思路:只能进行一次交易,最简单的想法就是,在每一天的时候,向前查看最低的股票价格,计算差额,然后就知道最大盈利日了
int maxProfit(vector<int>& prices) { if(prices.size() == 0) return 0; int minprice = prices[0]; int maxprofit = 0; for(int i = 0; i<prices.size(); i++){ if(prices[i] < minprice) minprice = prices[i]; if(prices[i] - minprice > maxprofit){ maxprofit = prices[i] - minprice; } } return maxprofit; }
2) 买卖股票的最佳时机 II
思路:能进行尽可能多的交易。
考虑买股票的策略:设今天价格p1,明天价格p2,若p1 < p2则今天买入明天卖出,赚取p2 - p1;
若遇到连续上涨的交易日,第一天买最后一天卖收益最大,等价于每天买卖(因为没有交易手续费);
遇到价格下降的交易日,不买卖,因此永远不会亏钱。
赚到了所有交易日的钱,所有亏钱的交易日都未交易,理所当然会利益最大化。
class Solution: def maxProfit(self, prices: List[int]) -> int: profit = 0 for i in range(1, len(prices)): tmp = prices[i] - prices[i - 1] if tmp > 0: profit += tmp return profit
一个通用方法团灭 6 道股票问题
300. Longest Increasing Subsequence 最长上升子序列
128. Longest Consecutive Sequence 最长连续序列
class Solution: def longestConsecutive(self, nums) -> int: if len(nums)<=1: return len(nums) nums = sorted(nums) dp = [1]*len(nums) for i in range(1, len(nums)): if nums[i]-nums[i-1]==1: dp[i] = dp[i-1]+1 elif nums[i]==nums[i-1]: dp[i] = dp[i-1] return max(dp)
牛客网 查找无重复最长子串
s = input() dp = [1]*len(s) for i in range(1,len(s)): temp = s[i-dp[i-1]:i] if s[i] not in temp: dp[i]=dp[i-1]+1 else: index = temp.index(s[i]) dp[i] = i-(index+i-dp[i-1]) print(max(dp))
322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
思路:假设硬币为1,2,5。 F(n)表示凑成n所需要的最少硬币数。则F(n) = min(F(n-1),F(n-2), F(n-3))+1。按照此思路代码如下:
class Solution:
def coinChange(self, coins, amount: int) -> int:
if len(coins)<1: return -1
if amount==0: return 0
dp = [-1]*(amount+1)
for i in range(1,amount+1):
if i in coins:
dp[i] = 1
else:
temp = amount+1
for j in coins: # 对每一种金额进行比较
if i-j>=0 and dp[i-j]>-1:
temp = min(temp, dp[i-j]+1)
if temp!=amount+1:
dp[i] = temp
return dp[-1]
与该题基本一个意思的题: 377. 组合总和 Ⅳ
139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
注意你可以重复使用字典中的单词。
思路:
dp[i]表示s到i位置是否可以由wordDict组成
所以有 如果dp[i - j]是true并且s[j:i]在wordDict里, 那么dp[i] = true;
用dp[i] 表示s[0:i]能否拆分。dp初始化为bool型。dp[0]=True。那么递推公式dp[i] = s[0:j] & s[j:i] = dp[j] & s[j:i]。根据这个式子,可以发现是两层循环:
class Solution:
def wordBreak(self, s: str, wordDict) -> bool:
if not s or len(wordDict)==0: return False
dp = [True] + [False]*len(s)
for i in range(1, len(s)+1):
for j in range(i):
if dp[j] and s[j:i] in wordDict: # 递推公式
dp[i] = True
break
return dp[-1]

浙公网安备 33010602011771号