背包之分组背包
分组背包是01背包的进阶问题,但是相对于较为简单,主要难在他的衍生问题。
分组背包就是现有n个物品,将这些物品分成若干组,给你一个容量为v的背包,对于每一个组中的物品,你最多只能选择一个,问哪些物品装入背包可以使得在体积总和不超过容量v的情况下,价值总和最大。
递推公式
可以看出分组背包和01背包有一点细微的差别,01背包是每一种物品最多拿一个,分组背包则变成了每一组最多能拿一个物品。现在我们定义dp[i][j]为前i组物品恰好放入容量为j的背包中的最优选择,dp[i][j]可以由前i - 1组的最优解加上这一组不取物品得到,也可以通过前i - 1个组容量为j - vol[i][k]的最优解再放入当前组的第k个物品得到,依次枚举k的大小,把这一组的所有物品全部遍历完毕,即可得到最终的dp[i][j],所以可以得到递推公式:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - vol[i][k]] + val[i][k])
其中vol[i][k]表示第i个组别中第k个元素的体积,val[i][k]表示第i个组别中第k个元素的价值。要得到当前的最优解,需要求出前i - 1个组能达到的最优解,然后再在前i - 1个组的基础上加入这一组的元素,对数组元素进行更新。
内存优化
模板一
对于二维的分组背包,我们可以对它进行一些内存优化,观察递归公式可以得到,每一次递推时只会用到两行一维数组,已经被计算出来的dp[i][j]在被下一行使用之后就不会再被调用,所以可以将dp[2][n]的二维数组优化为dp[2][n]。如下:
struct node {
ll u, v;
bool operator<(struct node a) const { return false; }
};
ll dp[N], m, capacity;
map<int, vector<node>>mp;
void solve() {
capacity = read(), m = read();
// 容量和物品件数
for (int i = 1; i <= m; i++) {
int a = read(), b = read(), c = read();
// 体积 价值 所属的组别
mp[c].push_back({a, b});
}
auto packBackPack = [](vector<node> &group) -> auto {
// 分组背包,group是被遍历的一组物品
for (int i = capacity; i >= 0; i--) {
// capacity是最大容量
for (int j = 0; j < group.size(); j++) {
if (i >= group[j].u) {
dp[i] = max(dp[i], dp[i - group[j].u] + group[j].v);
}
}
}
};
for (auto i : mp) {
packBackPack(i.second);
}
cout << dp[capacity];
}
int main() {
solve();
return 0;
}
二维数组dp[i]中的元素计算,需要使用二维数组dp[i - 1],所以直接使用一维数组dp,此时一维数组dp[i]表示容量为i时的最优解,再从容量开始从后往前遍历,已经遍历过的元素都会被更新为二维数组dp[i]中的答案,但是没有被遍历过的元素任然还是上一轮遍历时更新出来的二维数组dp[i - 1]中的元素,所以还是和递推公式中所用到的元素相同,但是节约了n * (n - 1)的空间大小。
模板二
struct node {
ll u, v;
bool operator<(struct node a) const { return false; }
};
ll dp[N], m, capacity;
map<int, vector<node>> mp;
void solve() {
capacity = read(), m = read();
// 容量和物品件数
for (int i = 1; i <= m; i++) {
int a = read(), b = read(), c = read();
// 体积 价值 所属的组别
mp[c].push_back({a, b});
}
auto packBackPack = [](vector<node> &group, int temp[]) -> auto {
// 分组背包,group是被遍历的一组物品
for (int i = 0; i < group.size(); i++) {
// capacity是最大容量
for (int j = capacity; j >= group[i].u; j--) {
dp[j] = max(dp[j], temp[j - group[i].u] + group[i].v);
}
}
};
for (auto i : mp) {
int temp[N];
for (int j = 0; j <= capacity; j++) {
temp[j] = dp[j];
}
packBackPack(i.second, temp);
}
cout << dp[capacity];
}
int main() {
solve();
return 0;
}
这个和上一个模板的思路相同,都是需要使用多少就开多少空间,通过记录二维数组dp[i - 1]计算出的元素,再交给当前的循环使用,来更新二维数组dp[i]中的元素,所以此时只需要两个一维数组分别记录二维数组dp[i - 1]已经更新出来的数据和存储循环中更新的二维数组dp[i]。
例题
P1757 通天之分组背包