写在前面
本人是垃圾211本科生,算法功底很弱,本文只是为了记录学习笔记而写,玻璃心轻点喷
下文代码均能通过平台测试,但绝对不是最优(仅供参考)
目录
01背包问题
问题描述
问题分析
由于每种物品只有 拿 和 不拿 两种状态,故称为01背包
①定义 DP 状态:
表示在 1 ~ i 个物品中任取,背包容量为 j 的时候的最大价值
②判断 DP 转移:
想要求得
如果新加入的物品 i 不放入背包,则
如果新加入的物品 i 放入背包,则
状态转移方程为:
在初学时脑海中出现过一个问题,背包问题的最优解不一定是刚刚好放满背包的,为什么 部分的重量是 ,万一 的最优解没有装满背包,岂不是浪费了一部分容量吗
解释:假设对于 1 ~ i - 1 的物品的最优解的总重量为 w,那么既然 的最优解没有装满背包,也就是说在 的基础上,增加了一部分背包容积到 是没有用处的,也就是说
③初始化 DP 数组:
初始全为0即可,代表最大价值为0(没有价值为负的物品)
④代码优化
将二维数组(行为物品,列为背包容量)通过滚动的形式变成一维数组
简单解释:二维数组的更新是一行一行更新的,每次更新新的元素取决于该元素的 上一层同列元素 以及 上一层前面列的元素。压缩成一维数组并且 从后向前遍历列 时,上一层同列元素等价于当前元素已有值,上一层前列元素等价于同一层前列元素(因为从后向前遍历时,还未更新到前面的元素,其位置仍未上一层的值)
相关例题
例题链接:01背包问题
#include <iostream>
#include <vector>
using namespace std;
int f[1001];
int main()
{
int N, V;
cin >> N >> V;
vector<int> weights(N + 1, 0);
vector<int> values(N + 1, 0);
for (int i = 1; i <= N; i++)
{
cin >> weights[i] >> values[i];
}
for (int i = 1; i <= N; i++)
{
for (int j = V; j >= weights[i]; j--)
{
f[j] = max(f[j], f[j - weights[i]] + values[i]);
}
}
cout << f[V] << endl;
return 0;
}
完全背包问题
问题描述
问题分析
由于每种物品可以选无限次,故称为完全背包
①定义 DP 状态:
表示在 1 ~ i 个物品中任取,背包容量为 j 的时候的最大价值
②判断 DP 转移:
采用思考角度二:想要求得
如果新加入的物品 i 不放入背包,则
如果新加入的物品 i 放入背包,则需要考虑放多少个,假设我们放入两个物品 i,则状态转移方程
和两个状态转移方程
的叠加是一致的。也就是说, 已由 更新过,不需要再去枚举此处放入多少个物品 i
状态转移方程为:
注意01背包和完全背包中状态转移方程的区别
01背包:
完全背包:
区别在于一个是 ,一个是
解释:因为 的最优解中有可能已经放入了物品 i,而01背包每个物品只能放入一次,故状态转移方程中必须使用
③初始化 DP 数组:
初始全为0即可,代表最大价值为0(没有价值为负的物品)
④代码优化
注意下面解释和01背包的相似性
将二维数组(行为物品,列为背包容量)通过滚动的形式变成一维数组
简单解释:二维数组的更新是一行一行更新的,每次更新新的元素取决于该元素的 上一层同列元素 以及 同一层前面列的元素。压缩成一维数组并且 从前向后遍历列 时,上一层同列元素等价于当前元素已有值,同一层前列元素等价于同一层前列元素(因为从前向后遍历时,已经更新到前面的元素,其位置已经是同一层元素的值)
相关例题
例题链接:完全背包问题
#include <iostream>
#include <vector>
using namespace std;
int f[1001];
int main()
{
int N, V;
cin >> N >> V;
vector<int> weights(N + 1, 0);
vector<int> values(N + 1, 0);
for (int i = 1; i <= N; i++)
{
cin >> weights[i] >> values[i];
}
for (int i = 1; i <= N; i++)
{
for (int j = weights[i]; j <= V; j++)
{
f[j] = max(f[j], f[j - weights[i]] + values[i]);
}
}
cout << f[V] << endl;
return 0;
}
多重背包问题
问题描述
问题分析
由于每种物品可以选特定次,故称为多重背包
本题主要聚焦于 二进制 和 单调队列 优化方法
多重背包完全可以转化成01背包来处理,将每种物品选若干次等价于有若干个价值一样的物品,每种物品只能选一次
二进制优化方法
现在考虑这样一种情况,假设我有3个价值为1的物品 ,最优解会选择2个价值为1的物品。那么此时,我选取 和选取 是完全一致的。
而直接使用1背包求解会重复计算上述两种情况(实际可能会更多)
所以一种想法是:采用二进制分组使拆分方法不会出现重复计算的情况
举例子说明:
- 6 = 1 + 2 + 3(不足4)
- 8 = 1 + 2 + 4 + 1(不足8)
- 18 = 1 + 2 + 4 + 8 + 3(不足16)
- 31 = 1 + 2 + 4 + 8 + 16
因为任何一个十进制数 num 都可以由若干个不重复的 求和得到,证明也很简单,因为每个十进制数都可以转化成二进制数,而二进制数字显然可以由若干个不重复的 求和得到
举例子说明:(1,2)表示 重量为1 价值为2 的物品
- 有33个(1,2) => 33 = 1 + 2 + 4 + 8 + 16 + 2(不足32)
- 等效于背包中有(1,2),(2,4),(4,8),(8,16),(16,32),(2,4)六件物品(33件 -> 6件)
- 如果最优解要选13个,那么13 = 1 + 4 + 8
- 如果最优解要选23个,那么23 = 1 + 2 + 4 + 16
- 如果最优解要选16个,那么16 = 16
可以看到,无论最优解最终选择多少个物品,都可以由这些二进制分组组成
单调队列优化方法(未完善)
单调队列类似优先队列,但优先队列本质为维护一个最大/最小堆,而单调队列的维护方法是自己定义的,并且单调队列插入元素的过程中有可能会弹出元素
一般实现如下:
(1)push(num),如果入口元素小于 num 则 弹出(直到队列为空或者出口元素大于等于num),完成后插入num
(2)pop(num),如果出口元素等于 num 则弹出,否则不做操作
(3)getMax(),出口元素即为队列中的最大值
不详细解释原理(下文给出了一种实现),相关题目链接:滑动窗口的最大值
class MonotonicQueue { public: // deque 为C++STL库中双向队列 deque<int> nums; void push(int num) { while ((int)nums.size() && nums.back() < num) { nums.pop_back(); } nums.push_back(num); } void pop(int num) { if (nums.size() == 0) return; if (nums.front() == num) nums.pop_front(); } int getMax() { return nums.front(); } };
首先,我们用最朴素的方法来实现多重背包
当决定物品 i 放几个的时候,枚举放0个到放最大个(背包放得下 + 个数足够)
故 为下列各个式子中的最大值:
放1个物品 i:
放2个物品 i:
放3个物品 i:
……………………
放k个物品 i:
k为满足 的最大值
观察公式,不难发现下列式子 模 weights[i] 是同余的
……………………
我们不妨假设
此时,我们将上面的状态转移式子写到头(当成完全背包来看):
放1个物品 i:
放2个物品 i:
放3个物品 i:
……………………
放k个物品 i:
……………………
放s个物品 i:
相关例题
例题链接:无优化_多重背包问题 I
#include <iostream>
#include <vector>
using namespace std;
int f[10001];
int main()
{
int N, V;
cin >> N >> V;
vector<int> weights;
vector<int> values;
for (int i = 0; i < N; i++)
{
int weight, value, num;
cin >> weight >> value >> num;
for (int j = 1; j <= num; j++)
{
weights.push_back(weight);
values.push_back(value);
}
}
for (int i = 0; i < weights.size(); i++)
{
for (int j = V; j >= weights[i]; j--)
{
f[j] = max(f[j], f[j - weights[i]] + values[i]);
}
}
cout << f[V] << endl;
return 0;
}
例题链接:二进制优化_多重背包问题 II
#include <iostream>
#include <vector>
using namespace std;
int f[10001];
int main()
{
int N, V;
cin >> N >> V;
vector<int> weights;
vector<int> values;
for (int i = 0; i < N; i++)
{
int weight, value, num;
cin >> weight >> value >> num;
int k = 1;
while (num >= k)
{
weights.push_back(k * weight);
values.push_back(k * value);
num -= k;
k *= 2;
}
if (num > 0)
{
weights.push_back(num * weight);
values.push_back(num * value);
}
}
for (int i = 0; i < weights.size(); i++)
{
for (int j = V; j >= weights[i]; j--)
{
f[j] = max(f[j], f[j - weights[i]] + values[i]);
}
}
cout << f[V] << endl;
return 0;
}
混合背包问题
问题描述
问题分析
由于有的物品能取一次,有的物品能取k次,有个物品能无限取,故称作混合背包
将问题简单化成多重背包
物品即便有无限次,最多也只能取到 背包容量 / 物品重量 的个数(向下取整)
相关例题
例题链接:混合背包问题
#include <iostream>
#include <vector>
using namespace std;
int f[10001];
int main()
{
int N, V;
cin >> N >> V;
vector<int> weights;
vector<int> values;
for (int i = 0; i < N; i++)
{
int weight, value, num;
cin >> weight >> value >> num;
if (num == 0)
num = V / weight;
if (num == -1)
num = 1;
int k = 1;
while (num >= k)
{
weights.push_back(k * weight);
values.push_back(k * value);
num -= k;
k *= 2;
}
if (num > 0)
{
weights.push_back(num * weight);
values.push_back(num * value);
}
}
for (int i = 0; i < weights.size(); i++)
{
for (int j = V; j >= weights[i]; j--)
{
f[j] = max(f[j], f[j - weights[i]] + values[i]);
}
}
cout << f[V] << endl;
return 0;
}
二维费用背包问题
问题描述
问题分析
不同于01背包,此时的背包需要同时考虑两个指标,所以需要在状态中增加一维存放第二种价值
相关例题
例题链接:二维费用背包问题
#include <iostream>
#include <vector>
using namespace std;
int f[101][101];
int main()
{
int N, V, M;
cin >> N >> V >> M;
vector<int> weights(N + 1, 0);
vector<int> values(N + 1, 0);
vector<int> volumns(N + 1, 0);
for (int i = 1; i <= N; i++)
{
cin >> volumns[i] >> weights[i] >> values[i];
}
for (int i = 1; i <= N; i++)
{
for (int j = V; j >= volumns[i]; j--)
{
for (int k = M; k >= weights[i]; k--)
{
f[j][k] = max(f[j][k], f[j - volumns[i]][k - weights[i]] + values[i]);
}
}
}
cout << f[V][M] << endl;
return 0;
}
分组背包问题
问题描述
问题分析
以下分析均本人胡思乱想,可能会有错误的地方
每一组中的物品只能选一个,我们 用一个物品来代替一组物品,该物品有 不同的表现形式 可以选择。举个例子,例如一组物品 [(1,2), (1,3), (4,4), (9,10)],将其等价于一个物品,该物品的重量和价值可以在四种之间选择
那么解题思路和01背包完全一致,不同之处在于最内层循环中,额外加入一层循环遍历该物品的若干种表现形式求最大值
相关例题
例题链接:分组背包问题
#include <iostream>
#include <vector>
using namespace std;
int f[101];
int main()
{
int N, V, S;
cin >> N >> V;
vector<vector<int>> weights(N + 1, vector<int>{0});
vector<vector<int>> values(N + 1, vector<int>{0});
for (int i = 1; i <= N; i++)
{
cin >> S;
for (int j = 0; j < S; j++)
{
int weight, value;
cin >> weight >> value;
weights[i].push_back(weight);
values[i].push_back(value);
}
}
for (int i = 1; i <= N; i++)
{
for (int j = V; j >= 0; j--)
{
for (int k = 1; k < weights[i].size(); k++)
{
if (j >= weights[i][k])
f[j] = max(f[j], f[j - weights[i][k]] + values[i][k]);
}
}
}
cout << f[V] << endl;
return 0;
}
有依赖的背包问题
问题描述
问题分析
本题需要分组背包和树形DP的知识
分组背包请看上文,树形DP请看 树形DP教程
选择一个物品的前提是选择该物品的父物品,故称为有依赖的背包问题
该题为典型的背包类树形DP(另一道典型题目:选课)
树形DP的核心就是当分析某个节点的时候,不考虑它的父节点,而是聚焦于当前节点的子树
①定义 DP 状态:
表示在考虑 i 节点的子树时,i 节点物品必须选择 并且分给该子树的背包容量为 j 时的最大价值
②判断 DP 转移:
首先,由于 i 节点物品必须选择,实际分给子节点的容量为
那么 就是 排列组合枚举子节点们分得的容量 中的最大值
例如,我们以2节点的子树为例(假设2节点的重量为3,4节点的重量为1,5节点的重量为2)
假设我们想要求得 ,容易想到的解决方法为:将7的容量分出3给父节点2,剩余的4分给两个子节点4和5(枚举 0+4,1+3,3+1,4+0 这些组合,找到一个最大值)
但是在子节点个数不确定时,枚举出若干排列组合只能用回溯算法求解,这时的复杂度一定超出了可行范围。于是,我们采用 分组背包的思想 优化问题
我们把4、5两个子节点看作两组物品,每组物品有5个,(weight, value):
第一组:
第二组:
问题转化为:每一组物品只能选择一个,背包总容量为4时的最大价值
上文的 等价于第一组物品选择第1个,第二组物品选择第5个
上文的 等价于第一组物品选择第3个,第二组物品选择第3个
由于 ,所以分组背包的最优解一定是:
上述式子中五个组合的其中一个
这时问题就变成了纯粹的分组背包问题,对于求解 来说,分组背包的组数为 ,背包容量为 ,每组的物品数为 ,每组第 i 个物品:
③实现细节:
在代码实现上,做了一点小小的优化,还是以刚才的例子说明:
问题 对应分给子树的容量为 ,在写代码的时候我们假设分给子树的容量就是7,计算后 先重新选择物品2修改一部分数据,后将非法的数据归零,即:
选择父节点物品,倒序 修改数据:
……………………
非法数据:
这样处理使得循环的边界条件得到了简化,具体请看下文的代码实现
相关例题
例题链接:有依赖的背包问题
#include <iostream>
#include <vector>
using namespace std;
class Node
{
public:
int weight;
int value;
vector<int> sons;
};
// 第 i 个节点选,并且给子树分配 j 个容量的最大价值
int f[101][101];
vector<Node> nodes(101, Node());
int N, V;
void dfs(int index)
{
// 分组背包外层循环:遍历分组
for (int i = 0; i < nodes[index].sons.size(); i++)
{
// 通过递归求解子树问题
int son = nodes[index].sons[i];
dfs(son);
// 分组背包中层循环:遍历背包容量
for (int j = V; j >= 0; j--)
{
// 分组背包内层循环:遍历组内物品
for (int k = 0; k <= j; k++)
{
// 第 k 个物品对应的重量和价值(k从0开始)
int weight = k;
int value = f[son][k];
f[index][j] = max(f[index][j], f[index][j - weight] + value);
}
}
}
// 必须选择父节点
for (int j = V; j >= nodes[index].weight; j--)
f[index][j] = nodes[index].value + f[index][j - nodes[index].weight];
// 处理非法节点
for (int j = 0; j < nodes[index].weight; j++)
f[index][j] = 0;
}
int main()
{
cin >> N >> V;
int root = -1;
nodes.resize(N + 1);
for (int i = 1; i <= N; i++)
{
int num;
cin >> nodes[i].weight >> nodes[i].value >> num;
if (num != -1)
nodes[num].sons.push_back(i);
root = num == -1 ? i : root;
}
dfs(root);
cout << f[root][V] << endl;
return 0;
}
背包问题求方案数
问题描述
问题分析
该题目求的是最优选的方案数,而不是最大价值,但解题思路大同小异
①定义 DP 状态:
首先思考,为什么会有多种最优选法。重新回顾01背包的状态转移方程:
不难发现,当出现 的情况时,就有了两条选择物品的分支。所以我们的解题思路是:开两个数组 f 和 nums,其中 表示选前 i 个物品,背包容量为 j 时的最大价值, 表示选前 i 个物品,背包容量为 j 时的最优方案数
② 判断 DP 转移:
(1):
第 i 个物品既可以选也可以不选,所以方案数为二者之和:
(2)
第 i 个物品必须不选,所以方案数为:
(3)
第 i 个物品必须要选,所以方案数为:
③ 初始化 DP 数组:
对于记录方案数的数组 nums,初值全部设置成1,因为一个物品也不放也是一种方案
相关例题
例题链接:背包问题求方案数
#include <iostream>
#include <vector>
using namespace std;
#define mod 1000000007
int f[1001];
int nums[1001];
int main()
{
int N, V;
cin >> N >> V;
vector<int> weights(N + 1, 0);
vector<int> values(N + 1, 0);
for (int i = 1; i <= N; i++)
{
cin >> weights[i] >> values[i];
}
for (int i = 0; i <= V; i++)
nums[i] = 1;
for (int i = 1; i <= N; i++)
{
for (int j = V; j >= 0; j--)
{
if (j < weights[i])
continue;
if (f[j - weights[i]] + values[i] == f[j])
{
nums[j] = (nums[j] + nums[j - weights[i]]) % mod;
}
else if (f[j - weights[i]] + values[i] > f[j])
{
nums[j] = nums[j - weights[i]];
}
f[j] = max(f[j], f[j - weights[i]] + values[i]);
}
}
cout << nums[V] << endl;
return 0;
}
背包问题求具体方案
问题描述
问题分析
题目要求输出 字典序最小的方案,考虑此条件,如果存在一种方案包含物品1,那么必须选择物品1,然后在 2 ~ N 个物品中选择一个最优解。所以我们反转01背包中的 DP 状态
①定义 DP 状态
表示在 i ~ N 个物品中任取,背包容量为 j 的时候的最大价值
②判断 DP 转移
想要求得
如果新加入的物品 i 不放入背包,则
如果新加入的物品 i 放入背包,则
状态转移方程为:
可以看到,状态转移方程中除了将 i - 1 变成了 i + 1,其余没有区别,所以代码书写只用反转外层遍历物品的顺序即可,不过需要注意的是本题不能使用滚动数组压缩成一维(如果压缩成一维则无法进行下面的回溯输出方案环节)
③回溯输出方案
首先,最终背包最大价值为
(1)如果最优方案必须要选择第一个物品,说明:。换句话说,由于最优方案必须选择物品1,那么计算 时,一定是由 计算得来
(2)如果最优方案必须不选第一个物品,说明:。换句话说,由于最优方案必须不选择物品1,那么计算 时,一定是由 计算得来
(3)如果既有选择第一个物品的最优方案,也有不选择第一个物品的最优方案,说明:。此时根据题意,我们需要求解字典序最小的方案,故 如果出现可选某个物品的情况,必须选择该物品
(4)判断完毕物品1,循环判断物品2:
如果物品1必须选择,则下一次判断
如果物品1必须不选,则下一次判断
相关例题
例题链接:背包问题求具体方案
#include <iostream>
#include <vector>
using namespace std;
// f[i][j]: 第 i ~ N 个物品,背包容量为 j
int f[1005][1005];
int main()
{
int N, V;
cin >> N >> V;
vector<int> weights(N + 1, 0);
vector<int> values(N + 1, 0);
for (int i = 1; i <= N; i++)
{
cin >> weights[i] >> values[i];
}
for (int i = N; i > 0; i--)
{
for (int j = 0; j <= V; j++)
{
if (j >= weights[i])
f[i][j] = max(f[i + 1][j], f[i + 1][j - weights[i]] + values[i]);
else
f[i][j] = f[i + 1][j];
}
}
vector<int> conditions(N + 1, 0);
int curN = 1;
int curV = V;
while (curN <= N)
{
// 必须选 curN 物品
if (curV >= weights[curN] && f[curN][curV] == f[curN + 1][curV - weights[curN]] + values[curN])
{
conditions[curN] = 1;
curV -= weights[curN];
}
// 不选 curN 物品
else if (f[curN][curV] == f[curN + 1][curV])
{
conditions[curN] = 0;
}
curN++;
}
for (int i = 1; i <= N; i++)
if (conditions[i])
cout << i << " ";
return 0;
}