完全背包
1 完全背包
题⽬来源: ⽜客⽹
题⽬链接: 【模板】完全背包
难度系数: ★★
链接:https://ac.nowcoder.com/acm/problem/226516
来源:牛客网
题目描述
你有一个背包,最多能容纳的体积是V。
现在有n种物品,每种物品有任意多个,第i种物品的体积为viv_ivi ,价值为wiw_iwi。
(1)求这个背包至多能装多大价值的物品?
(2)若背包恰好装满,求至多能装多大价值的物品?
输入描述:
第一行两个整数n和V,表示物品个数和背包体积。
接下来n行,每行两个数viv_ivi和wiw_iwi,表示第i种物品的体积和价值。
1≤n,V≤10001 \le n, V \le 10001≤n,V≤1000
输出描述:
输出有两行,第一行输出第一问的答案,第二行输出第二问的答案,如果无解请输出0。
示例1
输入
复制2 6 5 10 3 1
2 6
5 10
3 1
输出
复制10 2
10
2
示例2
输入
复制3 8 3 10 9 1 10 1
3 8
3 10
9 1
10 1
输出
复制20 0
20
0
说明
无法恰好装满背包。
示例3
输入
复制6 13 13 189 17 360 19 870 14 184 6 298 16 242
6 13
13 189
17 360
19 870
14 184
6 298
16 242
输出
复制596 189
596
189
说明
可以装5号物品2个,达到最大价值298*2=596,若要求恰好装满,只能装1个1号物品,价值为189.
【解法】
先解决第⼀问:
1. 状态表⽰:
dp[i][j] 表⽰:从前 i个物品中挑选,总体积不超过 j,所有的选法中,能挑选出来的最⼤价
值。(这⾥是和 01 背包⼀样哒)
那我们的最终结果就是 dp [n ][V] 。
2. 状态转移⽅程:
线性 dp 状态转移⽅程分析⽅式,⼀般都是根据最后⼀步的状况,来分情况讨论。但是最后⼀个物
品能选很多个,因此我们的需要分很多情况:
a. 选 0 个第 i 个物品:此时相当于就是去前 i − 1 个物品中挑选,总体积不超过 j 。此时最⼤
价值为 dp[i− 1][j ] ;
b. 选 1 个第 i 个物品:此时相当于就是去前 i − 1 个物品中挑选,总体积不超过 j − v [ i ] 。因
为挑选了⼀个 i 物品,此时最⼤价值为 dp[i− 1][j− v[i]] + w[i] ;
c. 选 2个第 个物品:此时相当于就是去前i-1 个物品中挑选,总体积不超过j − 2 × v[i]
。因为挑选了两个 物品,此时最⼤价值为 ; dp[i− 1][j− 2 × v[i]] + 2 × w[i]
d. ......
综上,状态转移⽅程为:
dp [i ][j ] = max (dp [i − 1][j ], dp [i − 1][j − v [i ]] + w [i ], dp [i − 1][j − 2 × v [i ]] + 2 × w [i]...
当计算⼀个状态的时候,需要⼀个循环才能搞定的时候,就要想到去优化。优化的⽅向就是⽤⼀个
或者两个状态来表⽰这⼀堆的状态,通常就是⽤数学的⽅式做⼀下等价替换。
观察发现 第⼆维是有规律的变化的 ,因此去看看 dp [ i ][ j − v [ i ]] 这个状态:
dp [i ][j − v [i ]] = max (dp [i − 1][j − v [i ]], dp [i − 1][j − 2 × v [i ]] + w [i ], dp [i − 1][j − 3 × v [i ]] + 2 × w [i]...)
我们发现,把 加上 正好和 中除了第⼀项以外的全部⼀致,因们可以
修改状态转移⽅程为:
dp [i ][j − v [i ]] w [i ] dp [i ][j]
dp [i ][j ] = max (dp [i − 1][j ], dp [i ][j − v [i ]] + w [i])
3. 初始化:
我们多加⼀⾏,⽅便我们的初始化,此时仅需将第⼀⾏初始化为0 即可。因为什么也不选,也能
满⾜体积不⼩于j 的情况,此时的价值为 0。
4. 填表顺序:
根据状态转移⽅程,我们仅需从上往下填表即可。
接下来解决第⼆问:
第⼆问仅需修改⼀下初始化以及最终结果即可。
1. 初始化:
因为有可能凑不⻬j 体积的物品,因此我们把不合法的状态设置为负⽆穷。这样在取最⼤值的时
候,就不会考虑到这个位置的值。负⽆穷⼀般设置为−0 x 3 f 3 f 3 f 3 f 即可。
然后把 dp [0][0] = 0 修改成0 ,因为这是⼀个合法的状态,最⼤价值是0 ,也让后续填表是正确
的。
2. 返回值:
在最后拿结果的时候,也要判断⼀下最后⼀个位置是不是⼩于 0 ,因为有可能凑不⻬。 不能判断是否等于 ,因为这个位置的值会被更新,只不过之前的值太⼩,导致更 新后还是⼩于 的。
【参考代码】
cpp
// 原始版本
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
// 第⼀问
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
}
cout << f[n][m] << endl;
// 第⼆问
memset(f, -0x3f, sizeof f);
f[0][0] = 0;
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
}
if(f[n][m] < 0) cout << 0 << endl;
else cout << f[n][m] << endl;
return 0;
}
2 疯狂的采药
题⽬来源: 洛⾕
题⽬链接: P1616 疯狂的采药
难度系数: ★
题目背景
此题为纪念 LiYuxiang 而生。
题目描述
LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:"孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。"
如果你是 LiYuxiang,你能完成这个任务吗?
此题和原题的不同点:
-
每种草药可以无限制地疯狂采摘。
-
药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!
输入格式
输入第一行有两个整数,分别代表总共能够用来采药的时间 t 和代表山洞里的草药的数目 m。
第 2 到第 (m+1) 行,每行两个整数,第 (i+1) 行的整数 ai,bi 分别表示采摘第 i 种草药的时间和该草药的价值。
输出格式
输出一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
输入输出样例
输入 #1复制
70 3
71 100
69 1
1 2
输出 #1复制
140
说明/提示
数据规模与约定
- 对于 30% 的数据,保证 m≤103。
- 对于 100% 的数据,保证 1≤m≤104,1≤t≤107,且 1≤m×t≤107,1≤ai,bi≤104。
【解法】
完全背包模版题。
1. 状态表⽰:
dp [ i ][ j ] 表⽰:从前 i 个药材中挑选,总时间不超过 j ,此时能采摘到的最⼤价值。
那么 dp [ n ][ m ] 就是结果。
2. 状态转移⽅程:
对于 i 位置的药材,可以选择采 0, 1, 2, 3... 个:
a. 选 0 个:最⼤价值为 dp [ i − 1][ j ] ;
b. 选 1 个:最⼤价值为 dp [ i − 1][ j − t [ i ]] + v [ i ] ;
c. 选 2 个:最⼤价值为 dp [ i − 1][ j − 2 × t [ i ]] + 2 × v [ i ] ;
d. ...
由于要的是最⼤价值,应该是上述所有情况的最⼤值。其中第⼆个往后的状态可以⽤
替代,因此状态转移⽅程为
dp [i ][j − t [i ]] + v [i]
dp [i ][j ] = max (dp [i − 1][j ], dp [i ][j − t [i ]] + v [i])
3. 初始化:
全部初始化 0 ,不影响后续填表的正确性。
4. 填表顺序:
从上往下每⼀⾏,每⼀⾏从左往右。
空间优化版本也是如此。
【参考代码】
cpp
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e4 + 10, M = 1e7 + 10;
int n, m;
int t[N], w[N];
LL f[M];
int main()
{
cin >> m >> n;
for(int i = 1; i <= n; i++) cin >> t[i] >> w[i];
for(int i = 1; i <= n; i++)
{
for(int j = t[i]; j <= m; j++)
{
f[j] = max(f[j], f[j - t[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
3 Buying Hay
题⽬来源: 洛⾕
题⽬链接: P2918 [USACO08NOV] Buying Hay S
难度系数: ★★
题目描述
约翰的干草库存已经告罄,他打算为奶牛们采购 H(1≤H≤50000) 磅干草。
他知道 N(1≤N≤100) 个干草公司,现在用 1 到 N 给它们编号。第 i 公司卖的干草包重量为 Pi(1≤Pi≤5,000) 磅,需要的开销为 Ci(1≤Ci≤5,000) 美元。每个干草公司的货源都十分充足, 可以卖出无限多的干草包。
帮助约翰找到最小的开销来满足需要,即采购到至少 H 磅干草。
输入格式
第 1 行:两个整数: N 与 H ,以空格分隔。
第 2 行至第 N+1 行:其中第 i+1 行包含两个整数: Pi 与 Ci ,以空格分隔。
输出格式
一个整数,表示 FJ 至少采购到 H 磅干草所需的最少花费。
输入输出样例
输入 #1复制
2 15
3 2
5 3
输出 #1复制
9
说明/提示
FJ 可以在第二家公司买 3 包干草,共花费 9 美元。
【解法】
完全背包简单变形。
1. 状态表⽰:
dp [i ][j ] 表⽰:从前 i 个⼲草公司中挑选,总重量⾄少为 j磅,此时的最⼩开销。
注意注意注意!这是我们遇到的第三类限制情况。
之前的限制条件为不超过 j ,或者是恰好等于 j 。这道题的限定条件是⾄少为 j ,也就是说可以
超过 j 。这会对我们分析状态转移⽅程的时候造成影响。
根据状态表⽰, dp[n][m] 就是结果。
2. 状态转移⽅程:
对于 i 位置的公司,可以选择买 0, 1, 2, 3... 个:
a. 选 0 个:开销为 dp [ i − 1][ j ] ;
b. 选 1 个:开销为 dp [ i − 1][ j − p [ i ]] + c [ i ] 。问题来了,状态表⽰⾥⾯是⾄少为 j ,也就是
说 j − p [ i ] ⼩于 0 也是合法的。因为公司提供了 p [ i ] 的重量,⼤于 j ,是符合要求的。但
是 dp 表的下标不能是负数,处理这种情况的⽅式就是对 j − p [ i ] 与 0 取⼀个最⼤值
max ( j − p [ i ], 0) 。当重量很⼤的时,只⽤去前⾯凑重量为 0 的就⾜够了,这样就符合我们
的状态表⽰了。因此,最终开销为 dp [ i − 1][ max (0, j − p [ i ])] + c [ i ]
c. 选 2 个:开销为 dp [ i − 1][ max (0, j − 2 × p [ i ])] + 2 × c [ i ] ;
d. ...
由于要的是最⼩开销,应该是上述所有情况的最⼩值。
其中第⼆个往后的状态可以⽤ dp [ i ][ max (0, j − p [ i ])] + c [ i ] 替代 ,因此状态转移⽅程为
dp [i ][j ] = max (dp [i − 1][j ], dp [i ][max (0, j − p [i ])] + c [i]) 。
3. 初始化:
全部初始化正⽆穷⼤ 0x 3f 3f 3f 3f ,然后 dp[0][0] = 0 , 不影响后续填表的正确性。
4. 填表顺序:
从上往下每⼀⾏,每⼀⾏从左往右。
空间优化版本也是如此。
【参考代码】
cpp
#include <iostream>
#include <cstring>
using namespace std;
const int N = 110, M = 50010;
int n, m;
int p[N], c[N];
int f[M];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> p[i] >> c[i];
memset(f, 0x3f, sizeof f);
f[0] = 0;
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
f[j] = min(f[j], f[max(0, j - p[i])] + c[i]);
}
}
cout << f[m] << endl;
return 0;
}
4 纪念品
题⽬来源: 洛⾕
题⽬链接: P5662 [CSP-J2019] 纪念品
难度系数: ★★★
题目描述
小伟突然获得一种超能力,他知道未来 T 天 N 种纪念品每天的价格。某个纪念品的价格是指购买一个该纪念品所需的金币数量,以及卖出一个该纪念品换回的金币数量。
每天,小伟可以进行以下两种交易无限次:
- 任选一个纪念品,若手上有足够金币,以当日价格购买该纪念品;
- 卖出持有的任意一个纪念品,以当日价格换回金币。
每天卖出纪念品换回的金币可以立即 用于购买纪念品,当日购买的纪念品也可以当日卖出换回金币。当然,一直持有纪念品也是可以的。
T 天之后,小伟的超能力消失。因此他一定会在第 T 天卖出所有纪念品换回金币。
小伟现在有 M 枚金币,他想要在超能力消失后拥有尽可能多的金币。
输入格式
第一行包含三个正整数 T,N,M,相邻两数之间以一个空格分开,分别代表未来天数 T,纪念品数量 N,小伟现在拥有的金币数量 M。
接下来 T 行,每行包含 N 个正整数,相邻两数之间以一个空格分隔。第 i 行的 N 个正整数分别为 Pi,1,Pi,2,...,Pi,N,其中 Pi,j 表示第 i 天第 j 种纪念品的价格。
输出格式
输出仅一行,包含一个正整数,表示小伟在超能力消失后最多能拥有的金币数量。
输入输出样例
输入 #1复制
6 1 100
50
20
25
20
25
50
输出 #1复制
305
输入 #2复制
3 3 100
10 20 15
15 17 13
15 25 16
输出 #2复制
217
说明/提示
样例 1 说明
最佳策略是:
第二天花光所有 100 枚金币买入 5 个纪念品 1;
第三天卖出 5 个纪念品 1,获得金币 125 枚;
第四天买入 6 个纪念品 1,剩余 5 枚金币;
第六天必须卖出所有纪念品换回 300 枚金币,第四天剩余 5 枚金币,共 305 枚金币。
超能力消失后,小伟最多拥有 305 枚金币。
样例 2 说明
最佳策略是:
第一天花光所有金币买入 10 个纪念品 1;
第二天卖出全部纪念品 1 得到 150 枚金币并买入 8 个纪念品 2 和 1 个纪念品 3,剩余 1 枚金币;
第三天必须卖出所有纪念品换回 216 枚金币,第二天剩余 1 枚金币,共 217 枚金币。
超能力消失后,小伟最多拥有 217 枚金币。
数据规模与约定
对于 10% 的数据,T=1。
对于 30% 的数据,T≤4,N≤4,M≤100,所有价格 10≤Pi,j≤100。
另有 15% 的数据,T≤100,N=1。
另有 15% 的数据,T=2,N≤100。
对于 100% 的数据,T≤100,N≤100,M≤103,所有价格 1≤Pi,j≤104,数据保证任意时刻,小伟手上的金币数不可能超过 104
【解法】
总策略:贪⼼。从前往后,⼀天⼀天的考虑如何最⼤⾦币。
因为纪念品可以在当天买,当天卖。因此所有的交易情况,就可以转换成"某天买隔天卖"的情况。那 么,我们就可以贪⼼的将每⼀天能拿到的最⼤利润全都拿到⼿:
• 从第⼀天开始,把第⼀天的⾦币看成限制条件,第⼆天的⾦币看成价值,求出:在不超过 m 的情
况下,能获得的最⼤价值 m 1 ;
• 然后时间来到第⼆天,把这⼀天的⾦币看成限制条件,第三天的⾦币看成价值,求出:在不超 m 1
的情况下,能获得的最⼤价值 m 2 ;
• 以此类推,直到把第 t − 1 天的情况计算出来,能获得最⼤价值就是结果。
接下来就处理,拿到第 i ⾏以及 i + 1 ⾏数据,在最⼤⾦币数量为 m 的前提下,获得的最⼤利润是
多少?
• 因为每⼀个纪念品都可以⽆限次购买;
• 把前⼀⾏看成限制,后⼀⾏减去前⼀⾏的值看成价值,就变成了标准的完全背包问题。
那我们的解决⽅法就是⼀⾏⼀⾏的跑完全背包,跑⼀⾏拿到最⼤价值,然后放到下⼀⾏继续跑,直到 跑完倒数第⼆⾏。
完全背包的逻辑:
1. 状态表⽰:
dp [ i ][ j ] 表⽰从前 i 个纪念品中挑选,总花费不超过 j 的情况下,最⼤的利润。
那么, dp [ n ][ m ] + m 就是能得到的最⼤⾦币数量。
2. 状态转移⽅程:
根据最后⼀个纪念品选的数量,分成如下情况:
a. 如果选 0 个:能获得的最⼤⾦币数量为 dp [ i − 1][ j ] ;
b. 如果选 1 个:能获得的最⼤⾦币数量为 dp [ i − 1][ j − w [ i ]] + v [ i ] − w [ i ] ;
c. 如果选 2 个:能获得的最⼤⾦币数量为 dp [ i − 1][ j − 2 × w [ i ]] + 2 × ( v [ i ] − w [ i ]) ;
d. ...
其中除了第⼀个状态外的所有状态都可以⽤ 来表⽰,⼜因为要的是
最⼤值,所以状态转移⽅程就是所有情况的最⼤值。
dp [i ][j − w [i ]] + v [i ] − w [i]
3. 初始化
全为 0 即可。
【参考代码】
cpp
#include <iostream>
#include <cstring>
using namespace std;
// 常量定义:
// N:纪念品数量上限(题目中N≤100)
// M:金币上限(题目中初始M≤1e4,逐天赚后也不会超1e4)
const int N = 110, M = 1e4 + 10;
int t, n, m; // t=T天,n=N种纪念品,m=当前金币数
int p[N][N]; // p[i][j]:第i天第j种纪念品的价格
int f[M]; // 完全背包的dp数组:f[j]表示花费≤j金币时的最大利润
// 完全背包函数:计算"第i天买、第i+1天卖"能赚的最大金币
// v[]:第i天的价格(成本),w[]:第i+1天的价格(卖出价),m:当前拥有的金币
int solve(int v[], int w[], int m) {
// 初始化dp数组为0:没花金币时利润为0
memset(f, 0, sizeof f);
// 遍历每种纪念品(完全背包:物品可无限选)
for(int i = 1; i <= n; i++) {
// 正序遍历金币数(完全背包和01背包的区别:正序!)
for(int j = v[i]; j <= m; j++) {
// 状态转移:
// f[j]:不买第i种纪念品的最大利润;
// f[j - v[i]] + (w[i] - v[i]):买至少1个第i种纪念品的最大利润(花v[i]成本,赚w[i]-v[i]利润);
// 取最大值,就是当前花费j金币的最大利润
f[j] = max(f[j], f[j - v[i]] + w[i] - v[i]);
}
}
// 返回:原有金币m + 最大利润f[m] → 第i+1天的总金币
return m + f[m];
}
int main() {
// 1. 读取输入:t天、n种纪念品、初始金币m
cin >> t >> n >> m;
// 读取t天的价格:p[i][j]表示第i天第j种纪念品的价格
for(int i = 1; i <= t; i++) {
for(int j = 1; j <= n; j++) {
cin >> p[i][j];
}
}
// 2. 贪心逐天计算:从第1天到第t-1天,计算每天→下一天的最大金币
for(int i = 1; i < t; i++) {
// 调用solve函数:p[i]是第i天价格(成本),p[i+1]是第i+1天价格(卖出价),当前金币m
m = solve(p[i], p[i + 1], m);
}
// 3. 输出最终金币数(第t天已卖出所有纪念品)
cout << m << endl;
return 0;
}