DP背包问题

目录

一、前言

二、0/1背包

1、装箱问题(lanqiaoOJ题号763)

2、2022(2022年国赛填空题,lanqiaoOJ题号2186)

三、完全背包

1、小明的背包2(lanqiaoOJ题号1175)

四、分组背包

五、多重背包


一、前言

本文主要讲了0/1背包、完全背包、分组背包、多重背包的相关概念。

二、0/1背包

1、装箱问题(lanqiaoOJ题号763)

【题目描述】

有一个箱子容量为 V (正整数,0≤V≤20000),同时有 n 个物品 (0<n≤30),每个物品有一个体积(正整数)。要求 n 个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。

【输入描述】

输入第一行,一个整数,表示箱子容量。第二行,一个整数 n,表示有 n 个物品。接下来 n 行,分别表示这 n 个物品的各自体积。

【输出描述】

输出一行,表示箱子剩余空间。

0/1背包的简化版,不管物品的价格。把体积 (不是价格) 看成最优化目标:最大化体积。

dp=[0]*20010
V=int(input())
n=int(input())
c=[0]*40
for i in range(1,n+1):
    c[i]=int(input())
for i in range(1,n+1):
    for j in range(V,c[i]-1,-1):
        dp[j]=max(dp[j],dp[j-c[i]]+c[i])
print(V-dp[V])

2、2022(2022年国赛填空题,lanqiaoOJ题号2186)

【题目描述】

将 2022 拆分成 10 个互不相同的正整数之和,总共有多少种拆分方法?注意交换顺序视为同一种方法。注意交换顺序视为同一种方法。

例如:

2022 = 1000 + 1022

2022 = 1022 + 1000

视为同一种方法。

【思路】

  • 题目求 10 个数的组合情况,这十个数相加等于 2022。因为是填空题可以不管运行时间,看起来可以用暴力 for 循环 10 次,加上剪枝。
  • 然而暴力的时间极长,因为答案是:379187662194355221。
  • 这一题其实是 0/1 背包:背包容量为 2022,物品体积为 1~2022,往背包中装 10 个物品,要求总体积为 2022,问一共有多少种方案。
  • 与标准背包的区别:是求方案总数。

定义dp[][[]:dp[i][j][k]表示数字 1~i 取 j 个,和为 k 的方案数。

下面的分析沿用标准 0/1 背包的分析方法。

从 i-1 扩展到 i,分两种情况:

1)k ≥ i。数 i 可以要,也可以不要。

      要 i。从 1~i-1 中取 j-1 个数,再取 i,等价于 dp[i-1][j-1][k-i]。

      不要 i。从 1~i-1 中取 j 个数,等价于 dp[i-1][j][k]

      合起来:dp[i][j][k] = dp[i-1][j][k] + dp[i-1][j-1][k-i]

2)k < i。由于数 i 比总和 k 还大,显然 i 不能用。有:dp[i][j][k] = dp[i-1][j][k]

【不用滚动数组】

1)k≥i。dp[i][j][k] = dp[i-1][j][k] + dp[i-1][j-1][k-i]

2)k<i。dp[i][j][k] = dp[i-1][j][k]

dp=[[[0]*2222 for i in range(11)] for j in range(2222)]
for i in range(0,2023):
    dp[i][0][0]=1       #特别注意这个初始化
for i in range(1,2023):
    for j in range(1,11):
        for k in range(1,2023):
            if k<i:
                dp[i][j][k]=dp[i-1][j][k]
            else:
                dp[i][j][k]=dp[i-1][j][k]+dp[i-1][j-1][k-i]
print(dp[2022][10][2022])

【用滚动数组】

1)k>=i。dp[i][j][k]=dp[i-1][j][k]+dp[i-1][j-1][k-i]

2)k<i。dp[i][j][k]=dp[i-1][j][k]

dp=[[0]*2222 for i in range(11)]
#for i in range(0,2023):
dp[0][0]=1               #特别注意这个初始化
for i in range(1,2023):
    for j in range(10,0,-1):    #10个数
        for k in range(i,2023): #k>=i
            dp[j][k]+=dp[j-1][k-i]
print(dp[10][2022])

三、完全背包

1、小明的背包2(lanqiaoOJ题号1175)

【题目描述】

小明有一个容量为 C 的背包。这天他去商场购物,商场一共有 N 种物品,第 i 种物品的体积为 ci,价值为 wi,每种物品都有无限多个。小明想知道在购买的物品总体积不超过 C 的情况下所能获得的最大价值为多少,请你帮他算算。

【输入描述】

输入第 1 行包含两个正整数 N,C,表示商场物品的数量和小明的背包容量。第 2~N+1 行包含 2 个正整数 c,w,表示物品的体积和价值。1<=N<=10^3,1<=C<=10^3,1<=ci, wi<=10^3。

【输出描述】

输出一行整数表示小明所能获得的最大价值。

  • 思路和 0/1 背包类似。0/1 背包的每种物品只有 1 件,完全背包的每种物品有无穷多件,第 i 种可以装 0 件、1 件、2 件、C/ci 件。
  • 定义 dp[i][j]:把前 i 种物品 (从第 1 种到第 i 种) 装入容量为 j 的背包中获得的最大价值。
  • 把每个 dp[i][j] 都看成一个背包:背包容量为 j,装 1~i 这些物品。最后得到的 dp[N][C] 就是问题的答案:把 N 种物品装进容量 C 的背包的最大价值。
  • 在 0/1 背包问题中,每个物品只有拿与不拿两种;而完全背包问题,需要考虑拿几个。

完全背包的代码和 0/1 背包的代码相似,只多了一个 k 循环,用来遍历每种物品拿几个。

def solve(n,C):
    for i in range(1,n+1):
        for j in range(0,C+1):
            if i==1:
                dp[i][j]=0
            else:
                dp[i][j]=dp[i-1][j]
            for k in range(0,j//c[i]+1):    #k*c[i]<=j  #在容量为j的背包中放几个
                dp[i][j]=max(dp[i][j],dp[i-1][j-k*c[i]]+k*w[i])
    return dp[n][C]

N=3011
dp=[[0]*N for j in range(N)]
w=[0]*N
c=[0]*N
n,C=map(int,input().split())
for i in range(1,n+1):
    c[i],w[i]=map(int,input().split())
print(solve(n,C))

复杂度:O(nC)

四、分组背包

分组背包问题:

  • 有一些物品,把物品分为 n 组,其中第 i 组第 k 个物品体积是 c[i][k],价值是 w[i][k];
  • 每组内的物品冲突,每组内最多只能选出一个物品;
  • 给定一个容量为 C 的背包,问如何选物品,使得装进背包的物品的总价值最大。

【解题思路】

解题思路与 0/1 背包相似。

  • 0/1 背包 dp[i][j]:把前 i 个物品 (从第1个到第i个) 装入容量为 j 的背包中获得的最大价值。
  • 分组背包 dp[i][j]:把前 i 组物品装进容量 j 的背包 (每组最多选一个物品),可获得的最大价值。
  • 状态转移方程:dp[i][j] = max{dp[i-1][j],dp[i-1][j-c[i][k]] + w[i][k]}

dp[i-1][j]表示第 i 组不选物品,dp[i-1][j-c[i][k]] 表示第 i 组选第 k 个物品。

求解方程需要做 i、j、k 的三重循环。

【滚动数组】

状态转移方程:dp[i][j]=max{dp[i-1][j], dp[i-1][j-c[i][k]]+w[i][k]}

用滚动数组,变为:dp[j]=max{dp[j], dp[j-c[i][k]]+w[i][k]}

dp=[0]*N
for i in range(1,n+1):      #遍历每个组
    for j in range(C,-1,-1):  #枚举容量
        for k in range(1,C+1):  #用k枚举第i组的所有物品
            if j>=c[i][k]:      #第k个物品能装进容量j的背包
                dp[j]=max(dp[j],dp[j-c[i][k]]+w[i][k])  #第i组第k个
print(dp[C])

五、多重背包

多重背包问题:

  • 给定 n 种物品和一个背包,第 i 种物品的体积是 ci,价值为 wi,并且有 mi 个,背包的总容量为 C。
  • 如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
  • 对比完全背包:一个容量为 C 的背包,有 N 种物品,第 i 种物品的体积为 ci,价值为 wi,每种物品都有无限多个。
  • 两者非常相似。

【解题思路1:转化为0/1背包】

【解题思路2:直接DP】

状态转移方程:dp[i][j]=max{ dp[i-1][j], dp[i-1][j-k*c[i]]+k*w[i] }

用滚动数组,变为:dp[j]=max{ dp[j], dp[j-k*c[i]]+k*w[i] }

dp=[0]*N
for i in range(1,n+1):      #枚举物品
    for j in range(C,c[i]-1,-1):  #枚举背包容量
        for k in range(1,m[i]+1):  #用k遍历第i组的所有物品
            if j>=c[i]*k:           #第k个物品能装进容量j的背包
                dp[j]=max(dp[j],dp[j-k*c[i]]+k*w[i])
print(dp[C])

【解题思路3:二进制拆分优化】

一种简单而有效的技巧。

  • 例如第 i 种物品有 mi=25 个,这 25 个物品放进背包的组合,有 0~25 的 26 种情况。
  • 不过要组合成 26 种情况,其实并不需要 25 个物品。
  • 根据二进制的计算原理,一个十进制整数 X,可以用 1、2、4、8、... 这些 2 的倍数相加得到,例如 25 = 16 + 8 + 1,这些 2 的倍数只有 logX 个。
  • 题目中第 i 种物品有 mi 个,用 logmi 个数就能组合出 0~mi 种情况。总复杂度从O(C* \sum_{i=1}^{n}mi)优化到O(C* \sum_{i=1}^{n}\log mi)。

 【二进制差分优化】

  • 注意拆分的具体实现,不能全部拆成 2 的倍数,而是先按 2 的倍数从小到大拆,最后是一个小于等于最大倍数的余数。
  • 保证拆出的数相加在 [1, mi] 范围内,不会大于mi。
  • 例如 mi =25,把它拆成1、2、4、8、10,最后是余数 10,10<16=24,这 5 个数能组合成 1~25 内的所有数字,不会超过 25。
  • 如果把 25 拆成 1、2、4、8、16,相加的范围就是 [1, 31] 了。

【例题】

【输入描述】

第一行是整数 n 和 C,分别表示物品种数和背包的最大容量。接下来 n 行,每行三个整数 wi、ci、mi,分别表示第 i 个物品的价值、体积、数量。

【输出描述】

输出一个整数,表示背包不超载的情况下装入物品的最大价值。

N=100010
w=[0]*N
c=[0]*N
m=[0]*N
xw=[0]*N
xc=[0]*N
xm=[0]*N    #新的

n,C=map(int,input().split())
for i in range(1,n+1):
    w[i],c[i],m[i]=map(int,input().split())   #到此输入完毕

#以下是二进制拆分
xn=0        #二进制拆分后的新物品总数量
for i in range(1,n+1):
    j=1
    while j<=m[i]:      #例:m[i]=2
        m[i]-=j         #减去已经拆分的
        xn+=1
        xc[xn]=j*c[i]   #新物品的体积
        xw[xn]=j*w[i]
        j<<=1           #二进制枚举:1,2,4...
    if m[i]>0:          #最后一个是余数
        xn+=1
        xc[xn]=m[i]*c[i]
        xw[xn]=m[i]*w[i]

#以下是滚动数组版本的0/1背包
dp=[0]*N
for i in range(1,xn+1):     #枚举物品
    for j in range(C,xc[i]-1,-1):       #枚举背包容量
        dp[j]=max(dp[j],dp[j-xc[i]]+xw[i])
print(dp[C])

以上,DP背包问题

祝好

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吕飞雨的头发不能秃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值