算法基础(背包问题)—分组背包和混合背包

分组背包

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∑n​ai​≤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][ jb [ i ]][ kc [ 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;
}
相关推荐
蓝色汪洋2 小时前
数码串和oj
数据结构·算法
努力学算法的蒟蒻2 小时前
day39(12.20)——leetcode面试经典150
算法·leetcode·面试
科学最TOP2 小时前
xLSTM-Mixer:基于记忆混合的多变量时间序列预测
大数据·人工智能·算法·机器学习·时间序列
你的冰西瓜2 小时前
C++中的vector容器详解
开发语言·c++·stl
刻BITTER2 小时前
C++ 获取任意整数类型的最大、最小值和长度
开发语言·c++
程序员老舅2 小时前
C++ STL 算法:从原理到工程实践
linux·c++·stl·c/c++·数据结构与算法
十五年专注C++开发2 小时前
ZeroMQ: 一款高性能、异步、轻量级的消息传输库
网络·c++·分布式·zeroqm
superman超哥2 小时前
仓颉语言中循环语句(for/while)的深度剖析与工程实践
c语言·开发语言·c++·python·仓颉
chenyuhao20242 小时前
Linux系统编程:线程概念与控制
linux·服务器·开发语言·c++·后端