【背包dp-----分组背包】------(标准的分组背包【可以不装满的 最大价值】)
通天之分组背包
题目链接
题目描述
自 01 01 01 背包问世之后,小 A 对此深感兴趣。一天,小 A 去远游,却发现他的背包不同于 01 01 01 背包,他的物品大致可分为 k k k 组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。
输入格式
两个数 m , n m,n m,n,表示一共有 n n n 件物品,总重量为 m m m。
接下来 n n n 行,每行 3 3 3 个数 a i , b i , c i a_i,b_i,c_i ai,bi,ci,表示物品的重量,利用价值,所属组数。
输出格式
一个数,最大的利用价值。
输入输出样例 #1
输入 #1
45 3
10 10 1
10 5 1
50 400 2
输出 #1
10
说明/提示
0 ≤ m ≤ 1000 0 \leq m \leq 1000 0≤m≤1000, 1 ≤ n ≤ 1000 1 \leq n \leq 1000 1≤n≤1000, 1 ≤ k ≤ 100 1\leq k\leq 100 1≤k≤100, a i , b i , c i a_i, b_i, c_i ai,bi,ci 在 int
范围内。
\
0、分组背包特点
- 每组内只能选 一个物品 或者 不选任何物品;
- 每组之间是独立的,可以按顺序处理;
- 对每组分别进行一次 0-1 背包更新;
- 最终使用动态规划来记录状态。
题解(二维dp)
一、状态定义
我们使用二维动态规划数组:
dp[i][j]
表示从前i
个物品组中,在总容量恰好为 j 的情况下所能获得的最大价值。
二、 初始化分析
for(ll j=0;j<=m;j++)
{dp[0][j]=0;
}
- 第 0 组表示“没有物品可选”,此时无论容量是多少,最大价值都为 0;
- 其他位置初始化为
LLONG_MIN
,表示不可达状态。
三、状态转移方程
对于当前组中的每一个物品 {w, v}
(重量和价值),我们尝试将其加入到容量 j
的背包中:
if (j >= w)
{dp[i][j] = max(dp[i][j], dp[i - 1][j - w] + v);
}
这个状态转移的意思是:
- 如果当前容量
j
可以装下当前物品,则从上一组的容量j - w
状态中转移过来,并加上该物品的价值; - 否则保持不变(继承上一组的状态)。
四、实现细节
1.数据结构说明
map<ll, vector<pair<ll, ll>>> groups;
- 使用
map
来将物品按组号分类; - 每个组是一个
vector<pair<weight, value>>
,存储该组中的所有物品。
2.动态规划数组初始化
vector<vector<ll>> dp(group_count + 1, vector<ll>(m + 1, LLONG_MIN));
for(ll j=0;j<=m;j++)
{dp[0][j]=0;
}
- 初始时,除了第 0 组外,其他所有状态设为最小值
LLONG_MIN
; - 第 0 组初始化为全 0,表示不选任何物品时的价值。
3.遍历过程的主次顺序
3.1. 外层循环:遍历物品组
- 主要任务:遍历每一个物品组(从第1组开始,跳过第0组)。
- 实现方式:
for (auto it = (++groups.begin()); it != groups.end(); it++) {ll gid = it->first;const vector<pair<ll, ll>>& items = it->second;// ... }
- 说明:这里的
it
是指向当前物品组的迭代器,gid
是当前组的组号,items
是该组内所有物品的集合。
3.2. 继承上一组的状态
- 主要任务:在处理当前组之前,首先继承前一组的状态(即假设不选择当前组中的任何物品)。
- 实现方式:
for (ll j = 0; j <= m; j++) {dp[gid][j] = dp[gid - 1][j]; }
- 说明:这一步确保了即使当前组没有合适的物品可选或者我们决定不选当前组的任何物品时,我们的解不会变差。
3.3. 中层循环:遍历当前组内的每个物品
- 主要任务:对于当前组中的每一个物品,尝试将其加入背包,并更新相应的状态。
- 实现方式:
for (const auto& item : items) {ll w = item.first; // 当前物品的重量ll v = item.second; // 当前物品的价值// ... }
3.4. 最内层循环:遍历背包容量
- 主要任务:对于当前物品,遍历所有可能的背包容量
j
,并根据当前物品的重量和价值更新dp
数组。 - 实现方式:
for (ll j = w; j <= m; j++) { // 注意这里从 w 开始循环if (dp[gid - 1][j - w] != LLONG_MIN){dp[gid][j] = max(dp[gid][j], dp[gid - 1][j - w] + v);} }
- 说明:这里从
w
开始循环是因为只有当背包容量大于等于当前物品的重量时,才有可能将该物品放入背包。
3.5.总结遍历过程的主次顺序
-
外层循环:遍历物品组
- 对于每一个物品组(从第1组开始),获取该组的所有物品。
-
继承上一组的状态
- 在处理当前组之前,先继承前一组的状态(即假设不选择当前组中的任何物品),确保基础状态正确。
-
中层循环:遍历当前组内的每个物品
- 对于当前组中的每一个物品,计算其对背包的影响。
-
最内层循环:遍历背包容量
- 对于当前物品,遍历所有可能的背包容量,并根据当前物品的重量和价值更新
dp
数组。
- 对于当前物品,遍历所有可能的背包容量,并根据当前物品的重量和价值更新
五、完整代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;int main()
{// m 表示背包的最大容量// n 表示物品总数ll m, n;cin >> m >> n;// 使用 map 将物品按组号分类存储// groups[group_id] 存储该组下的所有物品 {weight, value}map<ll, vector<pair<ll, ll>>> groups;// 插入一个无效的第0组,使后续组号可以直接和 dp 数组下标对应groups[0].push_back({0, 0});// 输入每个物品的信息:重量、价值、所属组号for (ll i = 0; i < n; i++) {ll weight, value, group_id;cin >> weight >> value >> group_id;groups[group_id].push_back({weight, value});}// 动态规划数组 dp[i][j]// dp[i][j] 表示前 i 组物品,在容量 j 下可以获得的最大价值// 初始化为 LLONG_MIN 表示不可达状态ll group_count = groups.size() - 1; // 减去人为添加的第0组vector<vector<ll>> dp(group_count + 1, vector<ll>(m + 1, LLONG_MIN));// 初始状态:前 0 组(没有选任何物品)时,所有容量下的最大价值都是 0for (ll j = 0; j <= m; j++) {dp[0][j] = 0;}// 遍历每一组(从实际的第1组开始)// 注意这里使用 ++groups.begin() 跳过第0组for (auto it = (++groups.begin()); it != groups.end(); it++) {ll gid = it->first; // 当前组号const vector<pair<ll, ll>>& items = it->second; // 该组的所有物品// 先继承上一组的状态(不选当前组中的任何物品)for (ll j = 0; j <= m; j++) {dp[gid][j] = dp[gid - 1][j];}// 对当前组中的每一个物品尝试选择for (const auto& item : items) {ll w = item.first; // 当前物品的重量ll v = item.second; // 当前物品的价值// 遍历所有可能的背包容量 j,从当前物品的重量开始for (ll j = w; j <= m; j++) {// 如果上一组 j - w 容量是可达的,则更新当前组 j 容量下的最大价值if (dp[gid - 1][j - w] != LLONG_MIN) {dp[gid][j] = max(dp[gid][j], dp[gid - 1][j - w] + v);}}}}// 输出结果:最后一组在最大容量 m 下所能获得的最大价值cout << dp[group_count][m] << endl;return 0;
}
六、 时间与空间复杂度分析
类型 | 复杂度 |
---|---|
时间复杂度 | O ( G ⋅ m ⋅ K ) O(G \cdot m \cdot K) O(G⋅m⋅K) |
空间复杂度 | O ( G ⋅ m ) O(G \cdot m) O(G⋅m) |
其中:
- G G G:物品组数;
- m m m:背包容量;
- K K K:平均每组中的物品数量。
题解(滚动数组优化)
一、 动态规划数组 dp[j]
- 定义:
dp[j]
表示在容量为j
的情况下可以获得的最大价值。 - 初始化:所有值初始化为
LLONG_MIN
(表示不可达状态),除了dp[0] = 0
(容量为0
时的最大价值为0
)。
2. 遍历主次顺序
2.1主循环:遍历每一组物品
for (auto it = groups.begin(); it != groups.end(); it++)
{const vector<pair<ll, ll>>& items = it->second;
- 遍历顺序:我们首先遍历每一个物品组,从第一个组到最后一个组依次处理。
- 原因:这样可以确保每组的状态转移依赖于前一组的状态,从而逐步构建最终的最优解。
2.2次循环:倒序处理容量 j
vector<ll> temp_dp(dp); // 先继承上一组的状态for (ll j = m; j >= 0; j--)
{for (const auto& item : items) {ll weight = item.first;ll value = item.second;if (j >= weight && dp[j - weight] != LLONG_MIN) {temp_dp[j] = max(temp_dp[j], dp[j - weight] + value);}}
}
- 倒序处理容量
j
:从大到小遍历容量j
,确保每次更新不会覆盖尚未使用的旧值。 - 原因:因为在动态规划中,我们需要基于未被当前操作修改过的旧值进行计算,倒序遍历可以避免数据覆盖的问题。
3. 使用临时 dp
数组
vector<ll> temp_dp(dp); // 先继承上一组的状态
-
为什么需要临时
dp
数组:- 在处理每一组时,我们需要先保存上一组的状态(即假设不选择当前组中的任何物品)。
- 如果直接在
dp
上进行更新,会导致后续的操作基于已经被修改的状态,从而影响结果的正确性。 - 使用临时数组
temp_dp
可以确保我们在处理当前组的所有物品之前,已经完整地保存了上一组的状态。
-
合并
temp_dp
到dp
:- 在处理完当前组的所有物品之后,我们将
temp_dp
合并回dp
,以完成状态转移。
- 在处理完当前组的所有物品之后,我们将
4. 输出结果
cout << *max_element(dp.begin(), dp.end()) << endl;
- 输出最大价值:由于我们的目标是找到最大可能的价值,而不是仅限于给定容量
m
,所以我们使用*max_element(dp.begin(), dp.end())
来获取整个dp
数组中的最大值。
5.完整代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;int main()
{// 输入 m 表示背包的最大容量// n 表示物品总数ll m, n; cin >> m >> n;// 使用 map 存储每组物品:// group_id -> vector<pair<weight, value>> 表示每个组中的所有物品map<ll, vector<pair<ll, ll>>> groups;// 读取每个物品的信息:重量、价值、所属组号for (ll i = 0; i < n; i++) {ll weight, value, group_id;cin >> weight >> value >> group_id;groups[group_id].push_back(make_pair(weight, value));}// 动态规划数组 dp[j]:// 表示当前状态下,容量为 j 时可以获得的最大价值// 初始值设为 LLONG_MIN 表示不可达的状态// dp[0] 初始化为 0 表示容量为 0 时的价值为 0vector<ll> dp(m + 1, LLONG_MIN);dp[0] = 0;// 遍历每一组物品for (auto it = groups.begin();it != groups.end(); ++it) {// 当前组的所有物品列表const vector<pair<ll, ll>>& items = it->second;// 创建 temp_dp 作为当前组的临时状态数组// temp_dp 初始化为当前 dp 的值(继承上一组的状态)// 这样做是为了保证我们可以不选当前组中的任何物品vector<ll> temp_dp(dp);// 倒序遍历容量 j(从大到小)// 原因:防止在更新 temp_dp[j] 时,前面已经计算过的结果被覆盖for (ll j = m; j >= 0; j--) {// 尝试从当前组中选择每一个物品for (const auto& item : items) {ll weight = item.first;ll value = item.second;// 如果当前容量 j 足够装下这个物品,并且 j - weight 状态可达if (j >= weight && dp[j - weight] != LLONG_MIN) {// 更新 temp_dp[j] 为:// 前一组容量 j - weight 的最大价值 + 当前物品价值// 取 max 是因为可能有多个物品竞争同一个 j 容量temp_dp[j] = max(temp_dp[j], dp[j - weight] + value);}}}// 将 temp_dp 合并回 dp 中,表示处理完这一组后的最终状态dp = temp_dp;}// 输出结果:整个 dp 数组中的最大值(不一定刚好用满容量 m)cout << *max_element(dp.begin(), dp.end()) << endl;return 0;
}