动态规划
1.斐波那契数列
2.记忆化搜索
3.动态规划
def f1(n):
if n == 0:
return 0
if n == 1:
return 1
return f1(n-1)+f1(n-2)
记忆化搜索
我们已经知道哥哥的递归计算方式的问题是重复计算,那么我们把已经计算过的内容存下来就好了,这种方法叫做记忆化搜索
memo = [-1]*(n+1)
def f2(n):
if n == 0:
return 0
if n == 1:
return 1
if memo[n] == -1:
memo[n] = f2(n-1)+f2(n-2)
return memo[n]
动态规划(DP)
将原问题拆解成若干个子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案
刚刚的两种方式都是一种自上而下思考问题的方式,比如在斐波那契数列里面,先考虑f5,然后考虑f4,…,f0,而动态规划一般来说是使用一种自下而上的思考方式
所以按照这种方式动态规划的计算逻辑
f(1)
f(2)
…
f(5)
def f3(n):
memo = [-1]*(n+1)
for i in range(0,n+1):
if i == 0:
memo[i] = 0
elif i ==1:
memo[i] = 1
else:
memo[i] = memo[i-1]+memo[i-2]
return memo[n]
对比
记忆化搜索和动态规划最大的不同是思考方式的不同,前者是自上而下,后者是自下而上
0-1背包问题
有个小偷偷东西,他的背包容量是C,物品的重量weight和价值对应value,请问小偷应该怎么偷东西才能获得价值最大的东西
C = 11
item | value | weight |
---|---|---|
1 | 1 | 1 |
2 | 6 | 2 |
3 | 18 | 5 |
4 | 22 | 6 |
5 | 28 | 7 |
状态和状态转移方程
动态规划常常适用于有重叠子问题和最优子结构性质的问题,所以很多时候 动态规划问题也是一个递归问题
状态:递归函数的定义
状态转移方程:根据当前的状态,推断出下一状态的方程
0-1背包问题
状态:定义一个状态p(i,w),该状态表示加入第i个物品,背包容量为w的时候的解,并且我们把最优解定义为m(i,w)
状态转移方程
当加入一个新物品i,在容量为w的情况下,它无非有两种可能性
1.当前的物品可以加入进来
m
(
i
,
w
)
=
m
(
i
−
1
,
w
)
+
v
i
m(i,w) = m(i-1,w)+v_i
m(i,w)=m(i−1,w)+vi
2.不把当前物品加入进来:m(i,w) = m(i-1,w)
最终可以得到的状态转移方程为
m ( i , w ) = { 0 if i = 0 0 if w = 0 m ( i − 1 , w ) if w i > w max { m ( i − 1 , w ) , m ( i − 1 , w − w i ) + v i } otherwise m(i, w)=\left\{\begin{array}{cc} 0 & \text { if } i=0 \\ 0 & \text { if } w=0 \\ m(i-1, w) & \text { if } w_{i}>w \\ \max \left\{m(i-1, w), m\left(i-1, w-w_{i}\right)+v_{i}\right\} & \text { otherwise } \end{array}\right. m(i,w)=⎩⎪⎪⎨⎪⎪⎧00m(i−1,w)max{m(i−1,w),m(i−1,w−wi)+vi} if i=0 if w=0 if wi>w otherwise
leetcode 416 分隔等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过100
数组的大小不会超过200
示例1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成[1, 5, 5]和[11].
示例2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
给定一个只包含整数的非空数组
是否可以从这个数组中挑选出一些正整数,每个数只能用一次,使得这些数的和等于整个数组元素的和的一半。
定义状态和状态转移方程
状态:定义dp[i][j]为[0,i]个物品中挑选若干个,能否使得这些数求和等于j
其中i表示当前步骤拿进来物品的item,j表示背包的容量,也就是正整数之和
状态转移方程
当nums[i]>j:dp[i][j] = dp[i-1][j]
当nums[i]<= j :dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]]
class Solution:
def canPartition(self,nums):
nums_sum = sum(nums)
if nums_sum %2 != 0:
return False
# 和的一半作为背包容量的大小
c = nums_sum // 2
dp = [[0]*(c+1) for _ in range(len(nums))]
# 对第一列数据进行处理
for i in range(len(nums)):
dp[i][0] = 1
# 对放进来的第一个数进行处理
if nums[0]<=c:
dp[0][nums[0]] = 1
for i in range(1,len(nums)):
num = nums[i]
for j in range(1,c+1):
if num > j:
dp[i][j] = dp[i-1][j]
continue
dp[i][j] = dp[i-1][j] or dp[i-1][j-num]
return dp[-1][-1]
# 优化一
# 将二维数组 只变成2行
def canPartition(self,nums):
nums_sum = sum(nums)
if nums_sum %2 != 0:
return False
# 和的一半作为背包容量的大小
c = nums_sum // 2
dp = [[0]*(c+1) for _ in range(2)]
# 对第一列数据进行处理
dp[0][0] ,dp[1][0] = 1,1
# 对放进来的第一个数进行处理
if nums[0]<=c:
dp[0][nums[0]] = 1
for i in range(1,len(nums)):
num = nums[i]
for j in range(1,c+1):
cur = i%2
pre = 1 if cur == 0 else 0
if num > j:
dp[cur][j] = dp[pre][j]
continue
dp[cur][j] = dp[pre][j] or dp[pre][j-num]
return dp[-1][-1]
# 优化二
def canPartition(self,nums):
nums_sum = sum(nums)
if nums_sum %2 != 0:
return False
# 和的一半作为背包容量的大小
c = nums_sum // 2
dp = [0]*(c+1)
# 对第一列数据进行处理
dp[0] = 1
# 对放进来的第一个数进行处理
if nums[0]<=c:
dp[nums[0]] = 1
for i in range(1,len(nums)):
num = nums[i]
for j in range(c,0,-1):
if num>j:
continue
dp[j] = dp[j] or dp[j-num]
return dp[-1]
# 最终优化
def canPartition(self,nums):
c = sum(nums)
if c%2 !=0:
return False
c = c//2
dp = [0]*(c+1)
dp[0] =1
for num in nums:
for i in range(c,num-1,-1):
dp[i] = dp[i] or dp[i-num]
return dp[-1]