分组背包
1 通天之分组背包
题⽬来源: 洛⾕
题⽬链接: P1757 通天之分组背包
难度系数: ★★
题目背景
直达通天路·小 A 历险记第二篇
题目描述
自 01 背包问世之后,小 A 对此深感兴趣。一天,小 A 去远游,却发现他的背包不同于 01 背包,他的物品大致可分为 k 组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。
输入格式
两个数 m,n,表示一共有 n 件物品,背包能承受的最大重量为 m。
接下来 n 行,每行 3 个数 ai,bi,ci,表示物品的重量,利用价值,所属组数。
输出格式
一个数,最大的利用价值。
输入输出样例
输入 #1复制
45 3
10 10 1
10 5 1
50 400 2
输出 #1复制
10
说明/提示
0≤m≤1000,1≤n≤1000,1≤k≤100,ai,bi,ci 在 int 范围内。
【解法】
跟之前的分析方式基本一致,相信你们自己就能把它做出来。因为一个组里面最多只能挑一个元素,所以我们就以一个组为单位。
状态表示:
dp [i][j] 表示从前 i 组中挑选物品,总重量不超过 j 的情况下,最大的价值。那么 dp [n][m] 就是最终结果。
状态转移方程:
根据第 i 组选什么物品,可以分若干情况讨论。设选择的物品重量为 a,价值为 b,此时的最大价值就是dp [i−1][j−a]+b。因为要的是最大值,所以考虑所有物品之后,取所有情况的最大值就是 dp [i][j]。
初始化:
全是 0 即可。
【参考代码】
cpp
#include <iostream>
#include <vector>
#include <algorithm> // 补充max函数依赖(部分编译器需显式导入)
using namespace std;
typedef pair<int, int> PII; // 存储物品:first=重量,second=价值
const int N = 1010; // 匹配m、n的最大值1000
int m, n, cnt; // m=总重量,n=物品数,cnt=最大组号
vector<PII> g[N]; // g[组号] = 该组的所有物品(重量,价值)
int f[N]; // 一维dp数组:f[j]表示重量不超过j的最大价值
int main() {
cin >> m >> n;
for (int i = 1; i <= n; i++) {
int a, b, c;
cin >> a >> b >> c;
cnt = max(c, cnt); // 更新最大组号(遍历所有组)
g[c].push_back({a, b}); // 物品按组归类
}
// 分组背包核心逻辑
for (int i = 1; i <= cnt; i++) { // 遍历每一组
// 逆序遍历重量:避免同一组物品被多次选择
for (int j = m; j >= 0; j--) {
// 遍历当前组的所有物品
for (auto& t : g[i]) {
int a = t.first, b = t.second;
if (j >= a) { // 重量足够时更新
f[j] = max(f[j], f[j - a] + b);
}
}
}
}
cout << f[m] << endl;
return 0;
}
2 排兵布阵
题⽬来源: 洛⾕
题⽬链接: P5322 [BJOI2019] 排兵布阵
难度系数: ★★★
题目描述
小 C 正在玩一款排兵布阵的游戏。在游戏中有 n 座城堡,每局对战由两名玩家来争夺这些城堡。每名玩家有 m 名士兵,可以向第 i 座城堡派遣 ai 名士兵去争夺这个城堡,使得总士兵数不超过 m。
如果一名玩家向第 i 座城堡派遣的士兵数严格大于对手派遣士兵数的两倍,那么这名玩家就占领了这座城堡,获得 i 分。
现在小 C 即将和其他 s 名玩家两两对战,这 s 场对决的派遣士兵方案必须相同。小 C 通过某些途径得知了其他 s 名玩家即将使用的策略,他想知道他应该使用什么策略来最大化自己的总分。
由于答案可能不唯一,你只需要输出小 C 总分的最大值。
输入格式
输入第一行包含三个正整数 s,n,m,分别表示除了小 C 以外的玩家人数、城堡数和每名玩家拥有的士兵数。
接下来 s 行,每行 n 个非负整数,表示一名玩家的策略,其中第 i 个数 ai 表示这名玩家向第 i 座城堡派遣的士兵数。
输出格式
输出一行一个非负整数,表示小 C 获得的最大得分。
输入输出样例
输入 #1复制
1 3 10
2 2 6
输出 #1复制
3
输入 #2复制
2 3 10
2 2 6
0 0 0
输出 #2复制
8
说明/提示
样例1解释:
小 C 的最佳策略为向第 1 座城堡和第 2 座城堡各派遣 5 名士兵。
样例2解释:
小 C 的最佳策略之一为向第 1 座城堡派遣 2 名士兵,向第 2 座城堡派遣 5 名士兵,向第 3 座城堡派遣 1 名士兵。
数据范围:
对于 10% 的数据: s=1,n≤3,m≤10
对于 20% 的数据: s=1,n≤10,m≤100
对于 40% 的数据: n≤10,m≤100
对于另外 20% 的数据: s=1
对于 100% 的数据:
1≤s≤100
1≤n≤100
1≤m≤20000
对于每名玩家 ai≥0,i=1∑nai≤m
【解法】
一个城堡一个城堡分析,对于第 i 个城堡,考虑派遣的人数应该在所有玩家对这个城堡派遣人数中考虑。比如示例二的第三个城堡,我们考虑派遣的人数就是 1 和 13(因为要严格大于两倍,大一点就是最好的)。
因此,把每一个城堡看成一个小组,所有玩家在这个城堡派遣的人数看成一个一个物品,要求的就是在派遣人数不超过 m 的情况下的最大得分,符合分组背包。
小优化:对每个城堡中玩家的派遣人数从小到大排序,这样在选择第 k 个人数的时候,总得分就是 k×i。
1. 状态表示:
dp[i][j] 表示:分配前 i 个城堡,在总人数不超过 j 的情况下,最大的得分。
那么 dp[n][m] 就是最终结果。
2. 状态转移方程:
根据第 i 个城堡分配的人数,分情况讨论。假设分配的是排序后的第 k 个元素,那么分配人数为 a[i][k],此时的最大得分是dp[i−1][j−a[i][k]] + i×k。
由于要的是最大值,状态转移方程就是上述所有合法的 k 里面的最大值。
3. 初始化:
全部为 0 即可。
【参考代码】
cpp
#include<iostream>
#include<algorithm>
using namespace std;
// 常量定义:N=城堡数上限,M=总人数上限(原M=2e4+10,需匹配数组维度)
const int N = 110;
const int M = 20010;
int s, n, m;
int a[N][N]; // a[i][k]:第i个城堡击败第k个玩家所需的人数
int f[N][M];
int main() {
cin >> s >> n >> m;
// 1. 输入每个玩家在对应城堡的派遣人数,计算击败所需人数(2倍+1)
for (int i = 1; i <= s; i++) { // i:玩家编号
for (int j = 1; j <= n; j++) { // j:城堡编号
cin >> a[j][i]; // 读入第j个城堡、第i个玩家的派遣人数
a[j][i] = a[j][i] * 2 + 1; // 击败该玩家需派遣的人数(严格>2倍)
}
}
// 2. 对每个城堡的击败人数从小到大排序(优化:便于按k递增枚举)
for (int i = 1; i <= n; i++) {
sort(a[i] + 1, a[i] + 1 + s);
}
// 3. 分组背包核心逻辑
for (int i = 1; i <= n; i++) { // 遍历每个城堡(每组)
for (int j = 0; j <= m; j++) { // 遍历总派遣人数
f[i][j] = f[i-1][j]; // 初始状态:不选第i个城堡的任何方案
// 枚举第i个城堡击败k个玩家的情况(k从1到s,且人数不超过j)
for (int k = 1; k <= s && a[i][k] <= j; k++) {
// 状态转移:选第i个城堡击败k个玩家,总得分=前i-1个城堡j-a[i][k]人数的得分 + k*i
f[i][j] = max(f[i][j], f[i-1][j - a[i][k]] + k * i);
}
}
}
// 输出:前n个城堡、总人数不超过m时的最大得分
cout << f[n][m] << endl;
return 0;
}
混合背包
1 樱花
题⽬来源: 洛⾕
题⽬链接: P1833 樱花
难度系数: ★★
题目背景
《爱与愁的故事第四弹·plant》第一章。
题目描述
爱与愁大神后院里种了 n 棵樱花树,每棵都有美学值 Ci(0<Ci≤200)。爱与愁大神在每天上学前都会来赏花。爱与愁大神可是生物学霸,他懂得如何欣赏樱花:一种樱花树看一遍过,一种樱花树最多看 Pi(0≤Pi≤100) 遍,一种樱花树可以看无数遍。但是看每棵樱花树都有一定的时间 Ti(0<Ti≤100)。爱与愁大神离去上学的时间只剩下一小会儿了。求解看哪几棵樱花树能使美学值最高且爱与愁大神能准时(或提早)去上学。
输入格式
共 n+1行:
第 1 行:现在时间 Ts(几时:几分),去上学的时间 Te(几时:几分),爱与愁大神院子里有几棵樱花树 n。这里的 Ts,Te 格式为:hh:mm,其中 0≤hh≤23,0≤mm≤59,且 hh,mm,n 均为正整数。
第 2 行到第 n+1 行,每行三个正整数:看完第 i 棵树的耗费时间 Ti,第 i 棵树的美学值 Ci,看第 i 棵树的次数 Pi(Pi=0 表示无数次,Pi 是其他数字表示最多可看的次数 Pi)。
输出格式
只有一个整数,表示最大美学值。
输入输出样例
输入 #1复制
6:50 7:00 3
2 1 0
3 3 1
4 5 4
输出 #1复制
11
说明/提示
100% 数据:Te−Ts≤1000(即开始时间距离结束时间不超过 1000 分钟),n≤10000。保证 Te,Ts 为同一天内的时间。
样例解释:赏第一棵樱花树一次,赏第三棵樱花树 2 次。
【解法】
分类讨论即可。
【参考代码】
cpp
#include <iostream>
#include <algorithm> // 补充max函数依赖(部分编译器需要)
using namespace std;
// 常量定义:
// N = 树的最大数量(题目中n≤10000)
// M = 最大可用时间(题目中Te-Ts≤1000分钟)
const int N = 10010, M = 1010;
// 全局变量(方便main函数内使用)
int n, m; // n=樱花树数量,m=可赏花的总时间(分钟)
int t[N], c[N], p[N]; // t[i]=第i棵树看1次的时间;c[i]=第i棵树看1次的美学值;p[i]=第i棵树最多看的次数(0=无限次)
int f[M]; // 核心dp数组:f[j]表示"花费j分钟赏花"能得到的最大美学值
int main()
{
// 变量说明:t1=开始小时,t2=开始分钟;t3=结束小时,t4=结束分钟;ch=读入冒号(无用)
int t1, t2, t3, t4;
char ch;
// 读入时间和树的数量:格式如"6:50 7:00 3"
cin >> t1 >> ch >> t2 >> t3 >> ch >> t4 >> n;
// 计算总可用时间(分钟):结束时间总分钟 - 开始时间总分钟
m = t3 * 60 + t4 - (t1 * 60 + t2);
// 读入每棵树的属性:共n行,每行Ti、Ci、Pi
for(int i = 1; i <= n; i++) {
cin >> t[i] >> c[i] >> p[i];
}
// 核心:分类型处理每棵树(背包问题核心逻辑)
for(int i = 1; i <= n; i++)
{
// 情况1:p[i]=0 → 完全背包(树可以看无限次)
if(p[i] == 0)
{
// 正序遍历时间j:从"看1次的时间"到"总时间"(允许重复选)
for(int j = t[i]; j <= m; j++)
{
// 更新f[j]:取"不选这棵树的得分"和"选这棵树的得分"的最大值
// 选这棵树的得分 = 花j-t[i]分钟的最大得分 + 本次看树的美学值
f[j] = max(f[j], f[j - t[i]] + c[i]);
}
}
// 情况2:p[i]≠0 → 多重背包/01背包(树最多看p[i]次)
else
{
// 逆序遍历时间j:从"总时间"到"看1次的时间"(避免重复选同一棵树)
for(int j = m; j >= t[i]; j--)
{
// 枚举看这棵树的次数k(1次到p[i]次,且k次的总时间≤j)
for(int k = 1; k <= p[i] && k * t[i] <= j; k++)
{
// 更新f[j]:选k次的得分 = 花j-k*t[i]分钟的最大得分 + k次的美学值
f[j] = max(f[j], f[j - t[i] * k] + c[i] * k);
}
}
}
}
// 输出:总时间m分钟内的最大美学值
cout << f[m] << endl;
return 0;
}
多维费⽤的背包问题
1. L 国的战⽃之间谍
题⽬来源: 洛⾕
题⽬链接: P1910 L 国的战⽃之间谍
难度系数: ★★
题目背景
L 国即将与 I 国发动战争!!
题目描述
俗话说的好:"知己知彼,百战不殆"。L 国的指挥官想派出间谍前往 I 国,于是,选人工作就落到了你身上。
你现在有 N 个人选,每个人都有这样一些数据:A(能得到多少资料)、B(伪装能力有多差)、C(要多少工资)。已知敌人的探查间谍能力为 M(即去的所有人 B 的和要小于等于 M)和手头有 X 元钱,请问能拿到多少资料?
输入格式
第一行三个整数 N,M,X 代表总人数,敌国侦察能力和总钱数。
第二行至第 N+1 行,每行三个整数 Ai,Bi,Ci 分别表示第 i 个人能得到的资料,他的伪装能力有多差和他要的工资。
输出格式
一行一个整数表示能得到的资料总数。
输入输出样例
输入 #1复制
3 10 12
10 1 11
1 9 1
7 10 12
输出 #1复制
11
说明/提示
对于 100% 的数据,1≤N≤100,1≤M≤1000,1≤X≤1000。
【解法】
⽆⾮就是在 01 背包的基础上多加了⼀维,那我们就把状态表⽰也加上⼀维即可。
1. 状态表⽰:
dp [ i ][ j ][ *k]*表⽰:从前i 个⼈中挑选,伪装能⼒之和不超过 j,总⼯资不超过k ,此时能获取
到的最多资料总数。
那么 dp [n ][m ][x] 就是结果。
2. 状态转移⽅程:
根据第 i 个⼈选或者不选分两种情况讨论:
a. 不选:此时的最多资料为 dp[i− 1][j][k] ;
b. 选:那就要去前i-1 各种,凑伪装能⼒之和不超过j-b[i] ,总⼯资不超过 k-c[i]时的最 多⼯资,再加上第 i个⼈的⼯资。也就是 dp [ i − 1][ j − b [ i ]][ k − c [ i ]] + a [ i ]。
取上述两种情况的最⼤值即可。注意第⼆种情况要特判⼀下。
【参考代码】
cpp
#include <iostream>
#include <algorithm> // 补充max函数(部分编译器需要)
using namespace std;
// 常量定义:
// N=人数上限(110),M=伪装上限(1010),X=工资上限(1010)
const int N = 110, M = 1010;
int n, m, x; // n=总人数,m=伪装上限,x=工资上限
int a[N], b[N], c[N]; // a[i]=第i人资料数,b[i]=伪装值,c[i]=工资
int f[M][M]; // 核心dp数组:f[j][k]表示"伪装≤j、工资≤k"时的最大资料数
int main()
{
// 第一步:读入总人数、伪装上限、工资上限
cin >> n >> m >> x;
// 第二步:读入每个人的3个属性(a=资料,b=伪装,c=工资)
for(int i = 1; i <= n; i++) {
cin >> a[i] >> b[i] >> c[i];
}
// 第三步:核心DP循环(二维01背包)
for(int i = 1; i <= n; i++) { // 遍历第i个人(逐个判断选不选)
for(int j = m; j >= b[i]; j--) { // 逆序遍历伪装上限(避免重复选同一人)
for(int k = x; k >= c[i]; k--) { // 逆序遍历工资上限
// 状态转移:选第i人 vs 不选第i人,取最大值
// 不选:f[j][k](保持原来的值)
// 选:f[j - b[i]][k - c[i]] + a[i](扣除伪装和工资,加上资料)
f[j][k] = max(f[j][k], f[j - b[i]][k - c[i]] + a[i]);
}
}
}
// 第四步:输出结果(伪装≤m、工资≤x时的最大资料数)
cout << f[m][x] << endl;
return 0;
}