一、0-1背包问题
https://www.acwing.com/problem/content/description/2/
思路:设状态dp[i][j]的含义是装入前i件物品装入容量为j的背包里时的最大总价值,那么状态转移方程为:
因为dp[i][j]只和dp[i-1][j]有关,所以状态可以转为一维的。在遍历j的时候,要逆序遍历,因为要保证较小的j是上一轮的状态:
比如i = 3, j = 8, v[3] = 5, w[3] = 1,一维的转移方程为dp[8] = max(dp[8],dp[3] + w[3]) 要保证dp[8]和dp[3]都是上一轮的状态。
class Solution:
def max_value(self, N, V_max, v, w):
# N: 物品总数
# V_max: 背包的容量
# v: 物品体积列表
# w: 物品价值列表
dp = [[0 for _ in range(V_max + 1)] for _ in range(N + 1)]
for i in range(1, N + 1):
for j in range(1, V_max + 1):
if j >= v[i]:
dp[i][j] = max(dp[i - 1][j - v[i]] + w[i], dp[i - 1][j])
else:
dp[i][j] = dp[i - 1][j]
return dp[-1][-1]
class Solution:
def max_value(self, N, V_max, v, w):
dp = [0] * (V_max + 1)
for i in range(1, N + 1):
for j in range(V_max, v[i] - 1, -1):
dp[j] = max(dp[j], dp[j - v[i]] + w[i])
return dp[-1]
二、完全背包问题
https://www.acwing.com/problem/content/description/3/
思路:与0-1背包问题不同的是,每件物品可以用无限次。0-1背包中,第i件物品拿还是不拿依赖于上一轮i-1的状态,这个状态绝对不会出现拿了第i个物品的情况,在j的循环中是逆序的,这保证了每件物品只拿一次。而完全背包问题中,在拿第i件物品时,可能正需要考虑已经拿了第i件物品且容量为j-v[i]的情况,即dp[i][j-v[i]],所以j要顺序遍历
class Solution:
def max_value(self, N, V_max, v, w):
dp = [0] * (V_max + 1)
for i in range(1, N + 1):
for j in range(v[i], V_max + 1):
dp[j] = max(dp[j], dp[j - v[i]] + w[i])
return dp[-1]
三、分割等和子集
此题为leetcode第416题
思路:此题完全可以和0-1背包问题类比起来。要想使两个子集的元素和相等,那么nums的所有元素和必须为偶数。设nums的所有元素加起来除2取整为sums,这里的sums可以认为是背包的体积,nums的元素个数为物品的个数,nums[i]为第i个物品所占的体积。设状态dp[i][j]的含义为,对于nums里前i个数,背包体积为j时,背包能否放满。如果不装nums[i]这个物品,那么能否装满背包取决于上个状态nums[i-1][j];如果装nums[i]这个物品,那么能否装满这个背包取决于nums[i-1][j-nums[i]]。类比于0-1背包问题,二维状态完全可以写成一维的状态。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if len(nums) == 1:
return False
n = len(nums)
sums = sum(nums)
if sums % 2 != 0:
return False
sums //= 2
dp = [[False for _ in range(sums + 1)] for _ in range(n)]
dp[0][nums[0]] = True
for i in range(1, n):
for j in range(sums + 1):
if nums[i] <= j:
dp[i][j] = dp[i - 1][j - nums[i]] or dp[i - 1][j]
else:
dp[i][j] = dp[i - 1][j]
return dp[-1][-1]
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if len(nums) == 1:
return False
n = len(nums)
sums = sum(nums)
if sums % 2 != 0:
return False
sums //= 2
dp = [False for _ in range(sums + 1)]
dp[nums[0]] = True
for i in range(1, n):
for j in range(sums, nums[i] - 1, -1):
dp[j] = dp[j] or dp[j - nums[i]]
return dp[-1]
四、目标和
此题为leetcode第494题
思路:也是类似于背包问题,设状态dp[i][j]的含义是,对于nums的前i个数,和为j的组合个数。对于nums[i],它可以添加正号或负号,因此状态转移方程由两部分组成:
d p [ i ] [ j ] = d p [ i − 1 ] [ j + n u m s [ i ] ] + d p [ i − 1 ] [ j − n u m s [ i ] dp[i][j]=dp[i-1][j+nums[i]]+dp[i-1][j-nums[i] dp[i][j]=dp[i−1][j+nums[i]]+dp[i−1][j−nums[i]
由于目标和s可能为负数,dp的下角标j为负数的话可能会有问题,我们这里给它添加一个偏置,先使目标和都为正数,然后将答案对应过去。设nums的总和为sums,那么nums里的元素经过加减后的范围是[-sums, +sums],总共有t = 2 * sums + 1种可能,因此状态dp是len(nums) * t维的。另外状态转移方程也要注意边界情况,j-sums[i]和j+sums[i]要在[0, t)之间。
这题当然也可以用一维状态解决,不过需要绕一下弯。设nums里面加正号的几个元素组成数组P,加符号的几个元素组成数组N,那么有sum§ – sum(N) = s,可得sum§ + sum(N) + sum§ – sum(N) = s + sum§ + sum(N),进一步得2 * sum§ = s + sum(nums),得到
s u m ( P ) = ( s + s u m ( n u m s ) ) 2 sum(P)=\frac{(s+sum(nums))}{2} sum(P)=2(s+sum(nums))
则问题转化为找到一个整数子集P,使得上式成立。根据上式,s + sum(nums)必须为偶数,否则返回0。问题转为0-1背包问题,组成sum§的方式有多少种,每个数字只取一次。设状态dp[i]的含义是,组成i的方式有多少种。状态转移方程为:
d p [ j ] = d p [ j ] + d p [ j − n u m s [ i ] ] dp[j]=dp[j]+dp[j-nums[i]] dp[j]=dp[j]+dp[j−nums[i]]
class Solution:
def findTargetSumWays(self, nums: List[int], s: int) -> int:
n = len(nums)
sums = sum(nums)
if s > sums:
return 0
t = sums * 2 + 1
dp = [[0 for _ in range(t)] for _ in range(n)]
dp[0][nums[0] + sums] = 1 # nums[0]+sums的组合个数,只有1个
dp[0][-nums[0] + sums] += 1 # 如果nums[0]为0,组合个数有2个,所以是+=
for i in range(1, n):
for j in range(t): # s可能是负的,sums相当于是个偏置
temp1 = dp[i - 1][j - nums[i]] if j - nums[i] >= 0 else dp[i - 1][0]
temp2 = dp[i - 1][j + nums[i]] if j + nums[i] < t else dp[i - 1][0]
dp[i][j] = temp1 + temp2
return dp[-1][s+sums]
class Solution:
def findTargetSumWays(self, nums: List[int], s: int) -> int:
n = len(nums)
sums = sum(nums)
if s > sums:
return 0
if (sum(nums) + s) % 2 == 1:
return 0
p = (s + sums) // 2
dp = [0] * (p + 1)
dp[0] = 1
for i in range(n):
for j in range(p, nums[i] - 1, -1):
dp[j] += dp[j - nums[i]]
return dp[-1]