动态规划系列:背包九讲,使用Python

01背包问题【简单】

AcWing传送门
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
0 < N , V ≤ 1000 0<N,V\leq1000 0<N,V1000 0 < v i , w i ≤ 1000 0<v_i,w_i\leq 1000 0<vi,wi1000

思路:

1. 状态:
二维数组 d p dp dp d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i 件物品,背包容量为 j 时的最优解。

2. 状态转移方程:

  • 倘若不选第 i 个物品, d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i1][j]
  • 倘若选第 i 个物品(需要满足 j ≥ v i j\geq v_i jvi), d p [ i ] [ j ] = d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] dp[i][j]=dp[i-1][j-v[i]]+w[i] dp[i][j]=dp[i1][jv[i]]+w[i]

只需取两者中较大值即为最优解。

3. 考虑初始条件:

  • dp大小: ( N + 1 ) × ( V + 1 ) (N+1)\times (V+1) (N+1)×(V+1)
  • 初始化:先全部赋值为0,这样至少体积为0或者不选任何物品的时候是满足要求 。

4. 考虑输出状态:
返回 d p [ − 1 ] [ − 1 ] dp[-1][-1] dp[1][1]

代码:

import sys
N,V=[int(_) for _ in sys.stdin.readline().split()]
# goods[:][0]存放容量信息,goods[:][1]存放价值信息
goods=[]
for _ in range(N):
    goods.append([int(_) for _ in sys.stdin.readline().split()])
    
dp=[[0]*(V+1) for _ in range(N+1)]
for i in range(1,N+1):
    for j in range(1,V+1):
        dp[i][j]=dp[i-1][j]
        if j>=goods[i-1][0]:
            dp[i][j]=max(dp[i][j],dp[i-1][j-goods[i-1][0]]+goods[i-1][1])
print(dp[-1][-1])

优化:

二维数组更新方式为:

dp[i][j] = dp[i - 1][j] # 不含i的所有选法的最大价值
if j >= v[i]: # 判断含i的选法是否成立
	dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i])

可以看出 d p [ i ] [ : ] dp[i][:] dp[i][:]只依赖于 d p [ i − 1 ] [ : ] dp[i-1][:] dp[i1][:],所以根本没必要保留之前的 d p [ i − 2 ] [ : ] dp[i-2][:] dp[i2][:]等状态值,因此使用一个一维数组即可。注意到,当我们更新索引值较大的dp值时,需要用到索引值较小的上一层dp值 d p [ j − v [ i ] ] dp[j - v[i]] dp[jv[i]];也就是说,在更新索引值较大的dp值之前,索引值较小的上一层dp值必须还在,还没被更新;所以只能索引从大到小更新。

代码:

import sys
N,V=[int(_) for _ in sys.stdin.readline().split()]
goods=[]
for _ in range(N):
    goods.append([int(_) for _ in sys.stdin.readline().split()])
    
dp=[0]*(V+1)
for i in range(N): # 需要更新N次
    for j in range(V,-1,-1):
        if j>=goods[i][0]:
            dp[j]=max(dp[j],dp[j-goods[i][0]]+goods[i][1])

print(dp[-1])

完全背包问题【简单】

AcWing传送门
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
0 < N , V ≤ 1000 0<N,V\leq1000 0<N,V1000 0 < v i , w i ≤ 1000 0<v_i,w_i\leq 1000 0<vi,wi1000

思路:

1. 状态:
二维数组 d p dp dp d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i 件物品,背包容量为 j 时的最优解。

2. 状态转移方程:

  • 第 i 个物品选 k 件, d p [ i ] [ j ] = max ⁡ 0 ≤ k v [ i ] ≤ j ( d p [ i − 1 ] [ j − k ∗ v [ i ] ] + k ∗ w [ i ] ) dp[i][j]=\max_{0\leq kv[i]\leq j}(dp[i-1][j-k*v[i]]+k*w[i]) dp[i][j]=0kv[i]jmax(dp[i1][jkv[i]]+kw[i])

3. 考虑初始条件:

  • dp大小: ( N + 1 ) × ( V + 1 ) (N+1)\times (V+1) (N+1)×(V+1)
  • 初始化:先全部赋值为0,这样至少体积为0或者不选任何物品的时候是满足要求 。

4. 考虑输出状态:
返回 d p [ − 1 ] [ − 1 ] dp[-1][-1] dp[1][1]

代码:

import sys
N,V=[int(_) for _ in sys.stdin.readline().split()]
goods=[]
for _ in range(N):
    goods.append([int(_) for _ in sys.stdin.readline().split()])
    
dp=[[0]*(V+1) for _ in range(N+1)]
for i in range(1,N+1):
    for j in range(1,V+1):
        k=j//goods[i-1][0] # 第i件商品最多可以选k件
        dp[i][j]=max([dp[i-1][j-x*goods[i-1][0]]+x*goods[i-1][1] for x in range(k+1)])

print(dp[-1][-1])    

优化1:
可以看出 d p [ i ] [ : ] dp[i][:] dp[i][:]只依赖于 d p [ i − 1 ] [ : ] dp[i-1][:] dp[i1][:],考虑使用一个一维数组。注意到,当我们更新索引值较大的dp值时,需要用到索引值较小的上一层dp值,也就是说,在更新索引值较大的dp值之前,索引值较小的上一层dp值必须还在,还没被更新;所以只能索引从大到小更新。

import sys
N,V=[int(_) for _ in sys.stdin.readline().split()]
goods=[]
for _ in range(N):
    goods.append([int(_) for _ in sys.stdin.readline().split()])
    
dp=[0]*(V+1)
for i in range(N):
    for j in range(V,-1,-1):
        k=j//goods[i][0] # 第i件商品最多可以选k件
        dp[j]=max([dp[j-x*goods[i][0]]+x*goods[i][1] for x in range(k+1)])

print(dp[-1])    

优化2:
对第 i 件物品,由状态转移方程:
{ d p [ i ] [ j − v ] = m a x ( d p [ i − 1 ] [ j − v ] , d p [ i − 1 ] [ j − 2 ∗ v ] + w , d p [ i − 1 ] [ j − 3 ∗ v ] + 2 ∗ w , . . . . ) d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w , . . . ) \begin{cases} dp[i][j-v]=max(dp[i-1][j-v],dp[i-1][j-2*v]+w,dp[i-1][j-3*v]+2*w,....)\\ dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w,dp[i-1][j-2*v]+2*w,...) \end{cases} {dp[i][jv]=max(dp[i1][jv],dp[i1][j2v]+w,dp[i1][j3v]+2w,....)dp[i][j]=max(dp[i1][j],dp[i1][jv]+w,dp[i1][j2v]+2w,...)
对比两项有,
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − v ] + w ) dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w) dp[i][j]=max(dp[i1][j],dp[i][jv]+w)

import sys
N,V=[int(_) for _ in sys.stdin.readline().split()]
goods=[]
for _ in range(N):
    goods.append([int(_) for _ in sys.stdin.readline().split()])
    
dp=[[0]*(V+1) for _ in range(N+1)]
for i in range(1,N+1):
    for j in range(1,V+1):
    	if j>=goods[i][0]:
        	dp[i][j]=max(dp[i-1][j],dp[i][j-goods[i][0]]+goods[i][1])

print(dp[-1][-1])    

另一种理解方式:将完全背包转化为01背包问题
具体地,对于 d p [ i ] [ j ] dp[i][j] dp[i][j],考虑到第 i i i 种物品最多选 ⌊ j / v i ⌋ \lfloor j/v_i\rfloor j/vi件,于是可以把第 i i i 种物品转化为 ⌊ j / v i ⌋ \lfloor j/v_i\rfloor j/vi件价值为 w i w_i wi的物品,即将一种物品拆分为多件只能选0件或1件的01背包问题。如果不放第 i i i 种物品,则 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i1][j];如果放入第 i 种物品,由于之前可能有第 i 种物品,则 d p [ i ] [ j ] = d p [ i ] [ j − v i ] + w i dp[i][j]=dp[i][j-v_i]+w_i dp[i][j]=dp[i][jvi]+wi
d p [ i ] [ j ] = max ⁡ { d p [ i − 1 ] [ j ] , 不 放 第 i 种 物 品 d p [ i ] [ j − v i ] + w i , j ≥ v i 至 少 放 一 件 第 i 种 物 品 dp[i][j]=\max\begin{cases}dp[i-1][j],\qquad\quad 不放第 i 种物品\\ dp[i][j-v_i]+w_i,\quad j\geq v_i 至少放一件第 i 种物品 \end{cases} dp[i][j]=max{dp[i1][j],idp[i][jvi]+wi,jvii

优化3:
将“优化2”中的dp优化为一维数组:

import sys
N,V=[int(_) for _ in sys.stdin.readline().split()]
goods=[]
for _ in range(N):
    goods.append([int(_) for _ in sys.stdin.readline().split()])
    
dp = [0 for i in range(V+1)]
for i in range(N):
    for j in range(V+1): # 从前往后
        if j >= goods[i][0]:
            dp[j] = max(dp[j], dp[j-goods[i][0]]+goods[i][1])

print(dp[-1])

多重背包问题

多重背包问题1【简单】

AcWing传送门
有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
0 < N , V ≤ 100 0<N,V\leq100 0<N,V100 0 < v i , w i , s i ≤ 100 0<v_i,w_i,s_i\leq100 0<vi,wi,si100

思路:

1. 状态:
二维数组 d p dp dp d p [ i ] [ j ] dp[i][j] dp[i][j]表示前i个物品,存入容量为j的背包里的最大价值。

2. 状态转移方程:

  • 第 i 个物品选 k 件, d p [ i ] [ j ] = max ⁡ 0 ≤ k v [ i ] ≤ j , k ≤ s i ( d p [ i − 1 ] [ j − k ∗ v [ i ] ] + k ∗ w [ i ] ) dp[i][j]=\max_{0\leq kv[i]\leq j,k\leq s_i}(dp[i-1][j-k*v[i]]+k*w[i]) dp[i][j]=0kv[i]j,ksimax(dp[i1][jkv[i]]+kw[i])

3. 考虑初始条件:

  • dp大小: ( N + 1 ) × ( V + 1 ) (N+1)\times (V+1) (N+1)×(V+1)
  • 初始化:先全部赋值为0,这样至少体积为0或者不选任何物品的时候是满足要求 。

4. 考虑输出状态:
返回 d p [ − 1 ] [ − 1 ] dp[-1][-1] dp[1][1]

代码:

import sys
N,V=[int(_) for _ in sys.stdin.readline().split()]
goods=[]
for _ in range(N):
    goods.append([int(_) for _ in sys.stdin.readline().split()])
    
dp=[[0]*(V+1) for _ in range(N+1)]
for i in range(1,N+1):
    for j in range(1,V+1):
        k=min(j//goods[i-1][0],goods[i-1][2]) # 第i件商品最多可以选k件
        dp[i][j]=max([dp[i-1][j-x*goods[i-1][0]]+x*goods[i-1][1] for x in range(k+1)])

print(dp[-1][-1])    

优化:
一维数组

import sys
N, V = [int(_) for _ in sys.stdin.readline().split()]
dp = [0]*(V+1)
for i in range(N):
    v, w, s = [int(_) for _ in sys.stdin.readline().split()]
    for j in range(V, -1, -1):
        k = 1
        while k <= s and j >= k * v:
            dp[j] = max(dp[j], dp[j - k*v] + k*w)
            k += 1
print(dp[-1])

多重背包问题2【中等】

AcWing传送门
有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
0 < N ≤ 1000 , 0 < V ≤ 2000 0<N\leq1000,0<V\leq2000 0<N1000,0<V2000 0 < v i , w i , s i ≤ 2000 0<v_i,w_i,s_i\leq2000 0<vi,wi,si2000

思路: 二进制拆分
这道题的数据范围如果用三层循环的话是达到了1e9,所以必须优化它。

可以把它转化为一个01背包的问题:每个物品有s件,我们可以把它差分成s份,每份物品当做不同的个体,即只能选一次,这就转化为了01背包物品。但是这样的话,物品个数变成了1000*2000=2e6,再循环一层空间的话,还是1e9的复杂度。

继续优化,一个物品的数量是s的话,只要把s拆分成一些数字,使它们能够表示出 1 ∼ s 1\sim s 1s中任意一个数字就可以,没必要把它拆成s个1。下面详细说明如何拆分。

首先给出以下事实: 1 到 2 n + 1 − 1 的 2^{n+1}−1的 2n+11所有数可以用 1 , 2 , 4 , 8 , . . . , 2 n 1,2,4,8,...,2^n 1,2,4,8,...,2n 中若干元素相加得到
∀ x ∈ { 1 , 2 , . . . , 2 n + 1 − 1 } , ∃ a 1 , . . . , a k s . t . x = 2 a 1 + . . . + 2 a k ( ∗ ) \forall\quad x\in\{1,2,...,2^{n+1}-1\},\quad\exist\quad a_1,...,a_k \quad s.t.\quad x=2^{a_1}+...+2^{a_k}\qquad(*) x{1,2,...,2n+11},a1,...,aks.t.x=2a1+...+2ak()

这是因为:(因为不太熟悉二进制,这段写的啰嗦,计算机专业可忽略~)

  • 1 ∼ 2 n + 1 − 1 1\sim 2^{n+1}-1 12n+11中任一个数均可对应一个 n+1 位二进制数表示;
  • 2 i 2^i 2i表示为 n+1 位二进制,除了第 i+1 位为1,其余均为0: 2 0 = 1 , 2 1 = 10 , 2 2 = 100 , 2 3 = 1000 , 2 4 = 10000 , . . . 2^0=1,2^1=10,2^2=100,2^3=1000,2^4=10000,... 20=1,21=10,22=100,23=1000,24=10000,...
  • 不妨设 x x x 的二进制表示第 a 1 , . . . , a k a_1,...,a_k a1,...,ak位为1,第 a i a_i ai 位为1的二进制数为 x a i x_{a_i} xai,则 x = x a 1 + . . . + x a k x=x_{a_1}+...+x_{a_k} x=xa1+...+xak,而 x a i x_{a_i} xai对应 2 a i − 1 2^{a_i-1} 2ai1

k = ⌈ l o g 2 s ⌉ k=\lceil log_2 s\rceil k=log2s,则
k = ⌈ l o g 2 s ⌉ ⇒ l o g 2 s < k + 1 ⇒ s < 2 k + 1 k=\lceil log_2 s\rceil\Rightarrow log_2s<k+1\Rightarrow s<2^{k+1} k=log2slog2s<k+1s<2k+1

下面说明: S = { 1 , 2 , . . . , 2 k − 1 , c = s − ( 1 + 2 + . . . + 2 k − 1 ) = s − ( 2 k − 1 ) } \mathcal{S}=\{1,2,...,2^{k-1},c=s-(1+2+...+2^{k-1})=s-(2^k-1)\} S={1,2,...,2k1,c=s(1+2+...+2k1)=s(2k1)}可以表示 1 ∼ s 1\sim s 1s中任意一个数。(**)

  • 首先由(*)可知 1 ∼ 2 k − 1 1\sim 2^k-1 12k1可由 S \mathcal{S} S中若干元素相加得到;
  • s s s 可由 S \mathcal{S} S中所有元素相加得到;
  • s − 1 = ( 1 + 2 + 2 2 + . . . + 2 k − 1 + c ) − 1 s-1=(1+2+2^2+...+2^{k-1}+c)-1 s1=(1+2+22+...+2k1+c)1,即 s − 1 s-1 s1可由 S \mathcal{S} S中除了1之外的元素加起来,类似的,可以可以减去表示出来的 1 ∼ 2 k − 1 1\sim 2^k-1 12k1 s − 2 = ( 1 + 2 + 2 2 + . . . + 2 k − 1 + c ) − 2 s-2=(1+2+2^2+...+2^{k-1}+c)-2 s2=(1+2+22+...+2k1+c)2,……,最小的数就是只剩下 s − ( 2 k − 1 ) s-(2^k-1) s(2k1),只需说明 s − ( 2 k − 1 ) ≤ 2 k s-(2^k-1)\leq 2^k s(2k1)2k即可,这等价于 s ≤ 2 k + 1 − 1 < 2 k + 1 s\leq 2^{k+1}-1<2^{k+1} s2k+11<2k+1,这是成立的。这样就说明了 [ 2 k , s ) [2^k,s) [2k,s)上的数也可由 S \mathcal{S} S中元素得到。

下面回到多重背包问题。
我们可以把 s s s 个相同的物品,拆分成 ⌈ l o g 2 s ⌉ + 1 \lceil log_2 s\rceil+1 log2s+1个不同的大物品(每个大物品只有一份),假设最优解中该物品要选 x x x 件,由(**),选择若干件大物品等价于选择 x x x 件该物品,而选若干件大物品是01背包问题。

代码:

import collections
good = collections.namedtuple('good', ['v', 'w'])
N, V = map(int, input().split())
Goods = []
dp = [0] * (V + 1)

for i in range(N):
    v, w, s = map(int, input().split())
    k = 1
    while k <= s:# k:1,2,2^2,...,2^{k-1}
        s -= k
        Goods.append(good._make([v*k, w*k]))
        k *= 2
    if s > 0: Goods.append(good._make([v*s, w*s])) # s=s-(1+2+2^2+...+2^{k-1})

for good in Goods:
    for j in range(V, good.v - 1, -1):
        dp[j] = max(dp[j], dp[j - good.v] + good.w)

print(dp[-1])

collections.namedtuple官方文档

# 类
Point = namedtuple('Point', ['x', 'y'])
# 实例
p = Point(11, y=22) 
# 从存在的序列创建一个新实例
t = [11, 22]
Point._make(t)

多重背包问题3【困难】

AcWing传送门
有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
0 < N ≤ 1000 , 0 < V ≤ 20000 0<N\leq1000,0<V\leq20000 0<N1000,0<V20000 0 < v i , w i , s i ≤ 20000 0<v_i,w_i,s_i\leq20000 0<vi,wi,si20000

思路: 单调队列优化


单调队列:

  • 单调队列 是指在一个队列中各个元素单调递增(或者递减),并且各个元素的下标单调递增
  • 不断地向缓存数组里读入元素,也不时地去掉最老的元素,不定期的询问当前缓存数组里的最小的元素。
  • 入队:队尾;出队:队首和队尾。

  进队时,将进队的元素为e,从队尾往前扫描,直到找到一个不大于e的元素d,将e放在d之后,舍弃e之后的所有元素;如果没有找到这样一个d,则将e放在队头(此时队列里只有这一个元素)。
  出队时,将出队的元素为e,从队头向后扫描,直到找到一个元素f比e后进队,舍弃f之前所有的。(实际操作中,由于是按序逐个出队,所以每次只需要出队只需要比较队头)。


多重背包问题的状态转移:
d p [ i ] [ j ] = max ⁡ 0 ≤ k ≤ k i j ( d p [ i − 1 ] [ j − k ∗ v [ i ] ] + k ∗ w [ i ] ) , k i j = min ⁡ { ⌊ j v i ⌋ , s i } dp[i][j]=\max_{0\leq k\leq k_{ij}}(dp[i-1][j-k*v[i]]+k*w[i]),\qquad k_{ij}=\min\{\lfloor \frac{j}{v_i}\rfloor,s_i\} dp[i][j]=0kkijmax(dp[i1][jkv[i]]+kw[i]),kij=min{vij,si}

实际上并不需要二维的dp数组,适当的调整循环条件,我们可以重复利用dp数组来保存上一轮的信息。

d p [ j ] dp[j] dp[j] 表示容量为 j j j 的情况下,获得的最大价值;那么,针对每一类物品 i ,都依次更新一下 d p [ V ] → d p [ 0 ] dp[V]\rightarrow dp[0] dp[V]dp[0] 的值,最后 d p [ V ] dp[V] dp[V] 就是一个全局最优值。对 d p [ j ] dp[j] dp[j],设 k j = min ⁡ { ⌊ j v i ⌋ , s i } k_j=\min\{\lfloor \frac{j}{v_i}\rfloor,s_i\} kj=min{vij,si}

d p [ j ] = max ⁡ { d p [ j ] , d p [ j − v i ] + w i , d p [ j − 2 v i ] + 2 w i , . . . , d p [ j − k j v i ] + k j w i } dp[j] = \max\{dp[j], dp[j-v_i] + w_i, dp[j-2v_i] + 2w_i, ...,dp[j-k_jv_i]+k_jw_i\} dp[j]=max{dp[j],dp[jvi]+wi,dp[j2vi]+2wi,...,dp[jkjvi]+kjwi}

下面阐述如何简化更新 d p [ V ] → d p [ 0 ] dp[V]\rightarrow dp[0] dp[V]dp[0] 的过程。

d p dp dp 数组分成 v i v_i vi个类:将 j % v i j\%v_i j%vi 余数相同的 d p [ j ] dp[j] dp[j]写在同一行,下面重新定义 k j = ( V − j ) / / v i k_j=(V-j)//v_i kj=(Vj)//vi(即有 j + k j v i ≤ V j+k_jv_i\leq V j+kjviV),
d p [ 0 ] d p [ v i ] d p [ 2 v i ] d p [ 3 v i ] . . . d p [ k 0 v i ] d p [ 1 ] d p [ v i + 1 ] d p [ 1 + 2 v i ] d p [ 1 + 3 v i ] . . . d p [ 1 + k 1 v i ] . . . . . . . . . . . . . . . . . . d p [ j ] d p [ j + v i ] d p [ j + 2 v i ] d p [ j + 3 v i ] . . . d p [ j + k j v i ] . . . . . . . . . . . . . . . . . . d p [ v i − 1 ] d p [ v i − 1 + v i ] d p [ v i − 1 + 2 v i ] d p [ v i − 1 + 3 v i ] . . . d p [ v i − 1 + k v i − 1 v i ] \begin{matrix} dp[0]& dp[v_i]& dp[2v_i]& dp[3v_i]& ... &dp[k_0v_i]\\ dp[1]&dp[v_i+1]& dp[1+2v_i]&dp[1+3v_i]& ... &dp[1+k_1v_i]\\ ...&...&...&...&...&...\\ dp[j]&dp[j+v_i]& dp[j+2v_i]&dp[j+3v_i]& ... & dp[j+k_jv_i]\\ ...&...&...&...&...&...\\ dp[v_i-1]& dp[v_i-1+v_i]& dp[v_i-1+2v_i]& dp[v_i-1+3v_i]& ... & dp[v_i-1+k_{v_i-1}v_i] \end{matrix} dp[0]dp[1]...dp[j]...dp[vi1]dp[vi]dp[vi+1]...dp[j+vi]...dp[vi1+vi]dp[2vi]dp[1+2vi]...dp[j+2vi]...dp[vi1+2vi]dp[3vi]dp[1+3vi]...dp[j+3vi]...dp[vi1+3vi]..................dp[k0vi]dp[1+k1vi]...dp[j+kjvi]...dp[vi1+kvi1vi]

由状态转移公式,对于 0 ≤ j < v i 0\leq j<v_i 0j<vi d p [ j + k v i ] ∀ k dp[j+kv_i] \forall k dp[j+kvi]k只依赖于
{ d p [ j ] , d p [ j + v i ] , d p [ j + 2 v i ] , d p [ j + 3 v i ] , . . . , d p [ j + k v i ] } \{dp[j],\quad dp[j+v_i],\quad dp[j+2v_i],\quad dp[j+3v_i],\quad...,\quad dp[j+kv_i]\} {dp[j],dp[j+vi],dp[j+2vi],dp[j+3vi],...,dp[j+kvi]}

具体地:
{ d p [ j ] = d p [ j ] d p [ j + v i ] = max ⁡ { d p [ j ] + w i , d p [ j + v i ] } d p [ j + 2 v i ] = max ⁡ { d p [ j ] + 2 w i , d p [ j + v i ] + w i , d p [ j + 2 v i ] } d p [ j + 3 v i ] = max ⁡ { d p [ j ] + 3 w i , d p [ j + v i ] + 2 w i , d p [ j + 2 v i ] + w i , d p [ j + 3 v i ] } . . . d p [ j + k j v i ] = max ⁡ { d p [ j ] + k j w i , d p [ j + v i ] + ( k j − 1 ) w i , . . . , d p [ j + ( k j − 1 ) v i ] + w i , d p [ j + k j v i ] } \begin{cases}\begin{aligned} dp[j]&=dp[j]\\ dp[j+v_i]&=\max\{dp[j]+w_i,dp[j+v_i]\}\\ dp[j+2v_i]&=\max\{dp[j]+2w_i,dp[j+v_i]+w_i,dp[j+2v_i]\}\\ dp[j+3v_i]&=\max\{dp[j]+3w_i,dp[j+v_i]+2w_i,dp[j+2v_i]+w_i,dp[j+3v_i]\}\\ ...\\ dp[j+k_jv_i]&=\max\{dp[j]+k_jw_i,dp[j+v_i]+(k_j-1)w_i,...,dp[j+(k_j-1)v_i]+w_i,dp[j+k_jv_i]\} \end{aligned}\end{cases} dp[j]dp[j+vi]dp[j+2vi]dp[j+3vi]...dp[j+kjvi]=dp[j]=max{dp[j]+wi,dp[j+vi]}=max{dp[j]+2wi,dp[j+vi]+wi,dp[j+2vi]}=max{dp[j]+3wi,dp[j+vi]+2wi,dp[j+2vi]+wi,dp[j+3vi]}=max{dp[j]+kjwi,dp[j+vi]+(kj1)wi,...,dp[j+(kj1)vi]+wi,dp[j+kjvi]}

这等价于:
{ d p [ j ] = d p [ j ] d p [ j + v i ] = max ⁡ { d p [ j ] , d p [ j + v i ] − w i } + w i d p [ j + 2 v i ] = max ⁡ { d p [ j ] , d p [ j + v i ] − w i , d p [ j + 2 v i ] − 2 w i } + 2 w i d p [ j + 3 v i ] = max ⁡ { d p [ j ] , d p [ j + v i ] − w i , d p [ j + 2 v i ] − 2 w i , d p [ j + 3 v i ] − 3 w i } + 3 w i . . . d p [ j + k j v i ] = max ⁡ { d p [ j ] , d p [ j + v i ] − w i , . . . , d p [ j + ( k j − 1 ) v i ] − ( k j − 1 ) w i , d p [ j + k j v i ] − k j w i } + k j w i \begin{cases}\begin{aligned} dp[j]&=dp[j]\\ dp[j+v_i]&=\max\{dp[j],dp[j+v_i]-w_i\}+w_i\\ dp[j+2v_i]&=\max\{dp[j],dp[j+v_i]-w_i,dp[j+2v_i]-2w_i\}+2w_i\\ dp[j+3v_i]&=\max\{dp[j],dp[j+v_i]-w_i,dp[j+2v_i]-2w_i,dp[j+3v_i]-3w_i\}+3w_i\\ ...\\ dp[j+k_jv_i]&=\max\{dp[j],dp[j+v_i]-w_i,...,dp[j+(k_j-1)v_i]-(k_j-1)w_i,dp[j+k_jv_i]-k_jw_i\}+k_jw_i \end{aligned}\end{cases} dp[j]dp[j+vi]dp[j+2vi]dp[j+3vi]...dp[j+kjvi]=dp[j]=max{dp[j],dp[j+vi]wi}+wi=max{dp[j],dp[j+vi]wi,dp[j+2vi]2wi}+2wi=max{dp[j],dp[j+vi]wi,dp[j+2vi]2wi,dp[j+3vi]3wi}+3wi=max{dp[j],dp[j+vi]wi,...,dp[j+(kj1)vi](kj1)wi,dp[j+kjvi]kjwi}+kjwi

更新 d p [ V ] → d p [ 0 ] dp[V]\rightarrow dp[0] dp[V]dp[0] 中的第 j 组的更新顺序为: d p [ j + k j + v i ] → d p [ j ] dp[j+k_j+v_i]\rightarrow dp[j] dp[j+kj+vi]dp[j] ,之所以逆序更新,是因为我们只用一个数组 d p dp dp存储数据,更新时使用前面值的旧值更新后面的值,倘若先更新前面的值,就会丢失前面值的旧值,无法更新后面的值。

倘若我们额外开辟一个数组,则可以正向更新,并且可以看出,后面值的更新的最大值操作对象是在前面值更新最大值操作对象中增加一个元素,每次入队的值是 d p [ j + k v i ] − k w i dp[j+kv_i]-kw_i dp[j+kvi]kwi

限制条件:设
max ⁡ { d p [ j ] , d p [ j + v i ] − w i , . . . , d p [ j + ( k − 1 ) v i ] − ( k − 1 ) w i , d p [ j + k v i ] − k w i } = d p [ j + k 0 v i ] − k 0 w i \max\{dp[j],dp[j+v_i]-w_i,...,dp[j+(k-1)v_i]-(k-1)w_i,dp[j+kv_i]-kw_i\}=dp[j+k_0v_i]-k_0w_i max{dp[j],dp[j+vi]wi,...,dp[j+(k1)vi](k1)wi,dp[j+kvi]kwi}=dp[j+k0vi]k0wi

d p [ j + k v i ] = d p [ j + k 0 v i ] + ( k − k 0 ) w i dp[j+kv_i]=dp[j+k_0v_i]+(k-k_0)w_i dp[j+kvi]=dp[j+k0vi]+(kk0)wi,也就是说,背包容量为 j + k v i j+kv_i j+kvi 时,仅考虑前 i 种背包,第 i 种背包选 k − k 0 k-k_0 kk0 个时价值最大。因此需要要求 k − k 0 ≤ s k-k_0\leq s kk0s

代码:

import collections
[N, V] = map(int, input().split())
dp = [0] * (V+1)
q = collections.deque()
for i in range(N):
    v, w, s = map(int,input().split())
    for j in range(v): # v个组
        q.clear()  # 注意每次新的循环都需要初始化队列
        num = (V-j)//v  # 剩余的空间最多还能放几个当前物品 j+num*v<=V
        for k in range(0,num+1):
            val = dp[k*v+j]-k*w
            while q and val >= q[-1][1]:
                q.pop()
            q.append([k,val])
            if q[0][0] < k-s:  # 存放的个数不能超出物品数量,否则弹出 
                q.popleft()
            dp[v*k+j] = q[0][1]+k*w
print(dp[V])

混合背包问题【中等】

AcWing传送门
有 N 种物品和一个容量是 V 的背包。物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 si 次(多重背包);

每种体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

输入格式:

  • 第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
  • 接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
    • s i = − 1 s_i=−1 si=1 表示第 i 种物品只能用1次;
    • s i = 0 s_i=0 si=0 表示第 i 种物品可以用无限次;
    • s i > 0 s_i>0 si>0 表示第 i 种物品可以使用 si 次;

输出格式:

  • 输出一个整数,表示最大价值。

数据范围: 0 < N , V ≤ 1000 , 0 < v i , w i ≤ 1000 , − 1 ≤ s i ≤ 1000 0<N,V\leq1000,0<v_i,w_i\leq1000,−1\leq si\leq1000 0<N,V1000,0<vi,wi1000,1si1000

思路:
全部转换为多重背包问题:

  • 01背包( s = − 1 s=-1 s=1): s = 1 s=1 s=1
  • 完全背包( s = 0 s=0 s=0): s = V / / v s=V//v s=V//v,能装下的最大数目;

代码:

import sys
N,V=[int(_) for _ in sys.stdin.readline().split()]
    
dp = [0 for i in range(V+1)]
for i in range(N):
    v, w, s = [int(_) for _ in sys.stdin.readline().split()]
    if s==-1:s=1
    if s==0:s=V//v
    for j in range(V, -1, -1):
        k = 1
        while k <= s and j >= k * v:
            dp[j] = max(dp[j], dp[j - k*v] + k*w)
            k += 1
print(dp[-1])

二维费用的背包问题【中等】

AcWing传送门
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。每件物品只能用一次。体积是 v i v_i vi,重量是 m i m_i mi,价值是 w i w_i wi

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。输出最大价值。

输入格式:

  • 第一行三个整数N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。
  • 接下来有 N 行,每行三个整数 v i , m i , w i v_i,m_i,w_i vi,mi,wi,用空格隔开,分别表示第 i 件物品的体积、重量和价值。

输出格式:

  • 输出一个整数,表示最大价值。

数据范围: 0 < N ≤ 1000 , 0 < V , M ≤ 100 , 0 < v i , m i ≤ 100 , 0 < w i ≤ 1000 0<N\leq1000,0<V,M\leq100,0<v_i,m_i\leq100,0<w_i\leq1000 0<N1000,0<V,M100,0<vi,mi100,0<wi1000

思路:

1. 状态:
三维数组 d p dp dp d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示前 i 件物品,背包容量为 j、承重为 k 时的最优解。

2. 状态转移方程:

  • 倘若不选第 i 个物品, d p [ i ] [ j ] [ k ] = d p [ i − 1 ] [ j ] [ k ] dp[i][j][k]=dp[i-1][j][k] dp[i][j][k]=dp[i1][j][k]
  • 倘若选第 i 个物品(需要满足 j ≥ v i , k ≥ m i j\geq v_i,k\geq m_i jvi,kmi), d p [ i ] [ j ] [ k ] = f [ i − 1 ] [ j − v [ i ] ] [ k − m [ i ] ] + w [ i ] dp[i][j][k]=f[i-1][j-v[i]][k-m[i]]+w[i] dp[i][j][k]=f[i1][jv[i]][km[i]]+w[i]

只需取两者中较大值即为最优解。

3. 考虑初始条件:

  • dp大小: ( N + 1 ) × ( V + 1 ) × ( M + 1 ) (N+1)\times (V+1)\times (M+1) (N+1)×(V+1)×(M+1)
  • 初始化:先全部赋值为0,这样至少体积为0或者不选任何物品的时候是满足要求 。

4. 考虑输出状态:
返回 d p [ − 1 ] [ − 1 ] [ − 1 ] dp[-1][-1][-1] dp[1][1][1]

代码:

import sys
N,V,M=[int(_) for _ in sys.stdin.readline().split()]
    
dp=[[[0]*(M+1) for _ in range(V+1)] for _ in range(N+1)]
for i in range(1,N+1):
    v,m,w=[int(_) for _ in sys.stdin.readline().split()]
    for j in range(1,V+1):
        for k in range(1,M+1):
            dp[i][j][k]=dp[i-1][j][k]
            if j>=v and k>=m:
                dp[i][j][k]=max(dp[i][j][k],dp[i-1][j-v][k-m]+w)
print(dp[-1][-1][-1])

优化:
二维数组。

import sys
N,V,M=[int(_) for _ in sys.stdin.readline().split()]
    
dp=[[0]*(M+1) for _ in range(V+1)]
for i in range(1,N+1):
    v,m,w=[int(_) for _ in sys.stdin.readline().split()]
    for j in range(V,-1,-1):
        for k in range(M,-1,-1):
            if j>=v and k>=m:
                dp[j][k]=max(dp[j][k],dp[j-v][k-m]+w)
print(dp[-1][-1])

分组背包问题【中等】

AcWing传送门
有 N 组物品和一个容量是 V 的背包。

  • 每组物品有若干个,同一组内的物品最多只能选一个。
  • 每件物品的体积是 v i j v_{ij} vij,价值是 w i j w_{ij} wij,其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。

输入格式:

  • 第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
  • 接下来有 N 组数据:
    • 每组数据第一行有一个整数 S i S_i Si,表示第 i 个物品组的物品数量;
    • 每组数据接下来有 S i S_i Si 行,每行有两个整数 v i j v_{ij} vij, w i j w_{ij} wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;

输出格式:

  • 输出一个整数,表示最大价值。

数据范围: 0 < N , V ≤ 100 , 0 < S i ≤ 100 , 0 < v i j , w i j ≤ 100 0<N,V\leq100,0<S_i\leq100,0<v_{ij},w_{ij}\leq100 0<N,V100,0<Si100,0<vij,wij100

思路:
1. 状态:
二维数组 d p dp dp d p [ k ] [ j ] dp[k][j] dp[k][j]表示前 k 组,背包容量为 j 时的最优解,对每一组,可以一件物品也不选择,也可以选择其中的一件物品。

2. 状态转移方程:

  • 倘若不选第 k 组的物品, d p [ k ] [ j ] = d p [ k − 1 ] [ j ] dp[k][j]=dp[k-1][j] dp[k][j]=dp[k1][j]
  • 倘若选第 k 组中的某个物品,如果选择第 i 件物品(需要满足 j ≥ v i j\geq v_i jvi),则 d p [ k ] [ j ] = d p [ k − 1 ] [ j − v [ i ] ] + w [ i ] dp[k][j]=dp[k-1][j-v[i]]+w[i] dp[k][j]=dp[k1][jv[i]]+w[i],应在第 k 组的物品中选择改值最大的一个,即 d p [ k ] [ j ] = max ⁡ 1 ≤ i ≤ s i { d p [ k − 1 ] [ j − v [ i ] ] + w [ i ] } dp[k][j]=\max_{1\leq i\leq s_i}\{dp[k-1][j-v[i]]+w[i]\} dp[k][j]=max1isi{dp[k1][jv[i]]+w[i]}

只需取两者中较大值即为最优解。

3. 考虑初始条件:

  • dp大小: N × ( V + 1 ) N\times (V+1) N×(V+1)
  • 初始化:先全部赋值为0,这样至少体积为0或者不选任何物品的时候是满足要求 。

4. 考虑输出状态:
返回 d p [ − 1 ] [ − 1 ] dp[-1][-1] dp[1][1]

代码:

import sys
[N,V]=[int(_) for _ in sys.stdin.readline().split()]
S=[]
groups=[[] for _ in range(N)]
for i in range(N):
    s=int(sys.stdin.readline().strip())
    S.append(s)
    for _ in range(s):
        groups[i].append([int(_) for _ in sys.stdin.readline().split()])

dp=[[0]*(V+1) for _ in range(N)]
for k in range(N):
    for j in range(1,V+1):
        dp[k][j]=dp[k-1][j]
        for i in range(S[k]):
            if j>=groups[k][i][0]:
                dp[k][j]=max(dp[k][j],dp[k-1][j-groups[k][i][0]]+groups[k][i][1])
print(dp[-1][-1])    

优化:
一维数组。

import sys
[N,V]=[int(_) for _ in sys.stdin.readline().split()]
S=[]
groups=[[] for _ in range(N)]
for i in range(N):
    s=int(sys.stdin.readline().strip())
    S.append(s)
    for _ in range(s):
        groups[i].append([int(_) for _ in sys.stdin.readline().split()])

dp=[0]*(V+1)
for k in range(N):
    for j in range(V,-1,-1):
        for i in range(S[k]):
            if j>=groups[k][i][0]:
                dp[j]=max(dp[j],dp[j-groups[k][i][0]]+groups[k][i][1])
print(dp[-1])

有依赖的背包问题【困难】

AcWing传送门
有 N 个物品和一个容量是 V 的背包。
物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点:如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。
在这里插入图片描述
每件物品的编号是 i,体积是 v i v_i vi,价值是 w i w_i wi,依赖的父节点编号是 p i p_i pi。物品的下标范围是 1…N。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。

输入格式:

  • 第一行有两个整数 N,V,用空格隔开,分别表示物品个数和背包容量。
  • 接下来有 N 行数据,每行数据表示一个物品。第 i 行有三个整数 v i , w i , p i v_i,w_i,p_i vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。如果 p i = − 1 p_i=−1 pi=1,表示根节点。 数据保证所有物品构成一棵树。

输出格式:

  • 输出一个整数,表示最大价值。

数据范围: 1 ≤ N , V ≤ 100 , 1 ≤ v i , w i ≤ 100 1\leq N,V\leq100,1\leq v_i,w_i\leq100 1N,V100,1vi,wi100
父节点编号范围:内部结点: 1 ≤ p i ≤ N 1\leq p_i\leq N 1piN;根节点 p i = − 1 p_i=−1 pi=1


树形动态规划:
树形动态规划就是在“树”的数据结构上的动态规划,平时作的动态规划都是线性的或者是建立在图上的,线性的动态规划有二种方向既向前和向后,相应的线性的动态规划有二种方法既顺推与逆推,而树形动态规划是建立在树上的,所以也相应的有二个方向:

  • → \rightarrow 根:在回溯的时候从叶子节点往上更新信息
  • → \rightarrow 叶:往往是在从叶往根dfs一遍之后(相当于预处理),再重新往下获取最后的答案。

思路: 树形dp+分组背包

  • d p ( i , j ) dp(i,j) dp(i,j)表示在节点 i 为根的子树中选择节点,总开销不超过 j 的所有选法中,最优选法的价值的最大和。
  • 在 i 子树上选择,根节点是必选的,剩下的容量需要分配给其子树。
  • 把每一个子树看成一个分组,每个分组中选择这个分组需要的容量,这些子树的总容量加起来不能超过 i 子树的剩下容量。这一步就转换为一个分组背包问题,物品就是选择的分配给子树的容量,价值就是这个子树在该容量约束下的最大子节点价值和。
  • 分组背包完成后,就可以得到一种最佳的剩余容量分配方案让 i 的所有子树的价值和最大,进而 d p ( i , j ) dp(i,j) dp(i,j)也就求出来了。

代码:

N, V = map(int, input().split())
arr = [(0, 0)] # 物品的体积、价值
link = {}  # 邻接表

root_id = -1
link = {i: [] for i in range(1, N + 1)} # i的值为其孩子结点
for i in range(1, N + 1):
    v, w, p = map(int, input().split())
    if p == -1:
        root_id = i
    else:
        link[p].append(i)

    arr.append((v, w))


from functools import lru_cache
@lru_cache(typed=False, maxsize=128000000)

def dp(i, j):
    if j < arr[i][0]: # 第i件物品的体积
        return 0

    # 根节点的收益
    ans = arr[i][1] # 第i件物品的价值
    left_j = j - arr[i][0] # 剩余价值
    if left_j == 0:
        return ans

    # 子节点进行分组背包
    if len(link[i]) == 0:
        return ans

    if len(link[i]) == 1:
        ans += dp(link[i][0], left_j)
        return ans

    n = len(link[i])
    f = [0] * (left_j + 1) # f(ii, jj)表示前ii棵子树总容量不超过jj的约束下,这些子树中选择节点的选法中,最优选法对应的最大的价值和
    for ii in range(n):
        for jj in range(left_j, -1, -1):
            if ii == 0:
                f[jj] = dp(link[i][ii], jj)
            else:
                for kk in range(1, jj + 1): # 组内
                    f[jj] = max(f[jj], f[jj - kk] + dp(link[i][ii], kk))
    ans += f[left_j]
    return ans
    
print(dp(root_id, V))

背包问题的方案数【中等】

AcWing传送门
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出 最优选法的方案数。注意答案可能很大,请输出答案模 109+7 的结果。

输入格式:

  • 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
  • 接下来有 N 行,每行两个整数 v i , w i v_i,w_i vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式:

  • 输出一个整数,表示 方案数 模 109+7 的结果。

数据范围: 0 < N , V ≤ 1000 , 0 < v i , w i ≤ 1000 0<N,V\leq1000,0<v_i,w_i\leq1000 0<N,V1000,0<vi,wi1000

思路:
1. 状态:

  • d p dp dp d p [ i ] dp[i] dp[i]表示背包容积为 i 时的最佳方案的总价值;
  • c n t cnt cnt c n t [ i ] cnt[i] cnt[i]表示背包容积为 i 时总价值为最佳的方案数。

2. 状态转移方程:

  • dp: d p [ j ] = max ⁡ ( d p [ j ] , d p [ j − v ] + w ) dp[j]=\max(dp[j],dp[j-v]+w) dp[j]=max(dp[j],dp[jv]+w)
  • cnt:如果体积为 j 时取得最大价值时不需要放当前物品,则 c n t [ j ] = c n t [ j ] cnt[j]=cnt[j] cnt[j]=cnt[j];如果体积为 j 时取得最大价值时需要放置当前物品,则 c n t [ j ] = c n t [ j − v ] cnt[j]=cnt[j-v] cnt[j]=cnt[jv];如果体积为 j 时放不放当前物品都能取得最大价值,则 c n t [ j ] = c n t [ j ] + c n t [ j − v ] cnt[j]=cnt[j]+cnt[j-v] cnt[j]=cnt[j]+cnt[jv]

3. 考虑初始条件:

  • dp: V + 1 V+1 V+1,全部初始化为0;
  • cnt: V + 1 V+1 V+1,全部初始化为1,因为背包里什么都不装也是一种方案。

代码:

mod = 10 ** 9 + 7
N, V = map(int, input().split())
dp = [0]*(V+1)
cnt = [1]*(V+1)  
for i in range(1,N+1):
    v, w = map(int, input().split())
    for j in range(V,v-1,-1):
        value=dp[j-v]+w
        if dp[j]<value:
            dp[j]=value
            cnt[j]=cnt[j-v]
        elif dp[j]==value:
            cnt[j]=(cnt[j]+cnt[j-v])%mod

print(cnt[-1])

求背包问题的具体方案【中等】

AcWing传送门
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。

输入格式:

  • 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
  • 接下来有 N 行,每行两个整数 v i , w i v_i,w_i vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式:

  • 输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。

物品编号范围是 1…N。

数据范围: 0 < N , V ≤ 1000 , 0 < v i , w i ≤ 1000 0<N,V\leq1000,0<v_i,w_i\leq1000 0<N,V1000,0<vi,wi1000

思路:
1. 状态:
题目要求输出字典序最小的解,假设存在一个包含第1个物品的最优解,为了确保字典序最小那么我们必然要选第一个。那么问题就转化成从2~N这些物品中找到最优解。因此定义状态:

  • dp: d p [ i ] dp[i] dp[i]表示第 i 件物品到最后一件物品总容量为 j 的最优解。

2. 状态转移方程:

  • 不选第 i 件物品,那么最优解等同于从第 i+1 件物品到最后一个元素总容量为 j 的最优解;
  • 第二种是选了第 i 个物品,那么最优解等于当前物品的价值 w i w_i wi加上从第 i+1 个物品到最后一个元素总容量为 j − v i j−v_i jvi的最优解。

d p [ i ] [ j ] = max ⁡ ( d p [ i + 1 ] [ j ] , d p [ i + 1 ] [ j − v ] + w ) dp[i][j]=\max(dp[i+1][j],dp[i+1][j-v]+w) dp[i][j]=max(dp[i+1][j],dp[i+1][jv]+w)

3. 考虑初始条件:
dp: V + 1 V+1 V+1。初始化为0。

4. 考虑输出状态:
d p [ 1 ] [ V ] dp[1][V] dp[1][V]为最大价值。

  • 如果 d p [ 1 ] [ V ] = d p [ 2 ] [ V − v [ i ] ] + w [ i ] dp[1][V]=dp[2][V-v[i]]+w[i] dp[1][V]=dp[2][Vv[i]]+w[i],说明选取了第1个物品可以得到最优解。
  • 如果 d p [ 1 ] [ V ] = d p [ 2 ] [ V ] dp[1][V]=dp[2][V] dp[1][V]=dp[2][V],说明不选取第一个物品才能得到最优解。
  • 如果 d p [ 1 ] [ V ] = d p [ 2 ] [ V − v [ i ] ] + w [ i ] = d p [ 2 ] [ V ] dp[1][V]=dp[2][V-v[i]]+w[i]=dp[2][V] dp[1][V]=dp[2][Vv[i]]+w[i]=dp[2][V],说明选不选都可以得到最优解,但是为了考虑字典序最小,我们也需要选取该物品。

代码:

N,V = list(map(int, input().split()))
w, v = [], []
for i in range(N):
    vi, wi = list(map(int, input().split()))
    w.append(wi)
    v.append(vi)
    
dp = [[0 for _ in range(V+1)]for _ in range(N+1)]
# 根据递推公式知道物品要倒排
for i in range(N-1, -1, -1):
    for j in range(1,V+1):
        dp[i][j] = dp[i+1][j]
        if j >= v[i]:
            dp[i][j] = max(dp[i][j], dp[i+1][j-v[i]]+w[i])

cur_v = V
ans = []
for i in range(N):
    if dp[i][cur_v] == dp[i+1][cur_v-v[i]]+w[i]:
        ans.append(str(i+1))
        cur_v -= v[i]
print(" ".join(ans))

AcWing 6. 多重背包问题 III——对单调队列优化的一些理解
AcWing 6. 多重背包问题 III 详解 + yxc大佬代码解读
树形dp总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值