终于是完结了AC自动机,接下来开个新坑——背包问题,背包的种类还是很多的,之前有学过,但都是这里看一点,那里看一点,导致现在都搞混了,所以重新系统看看这方面的内容。
先从简单的入手——01背包,那么为什么叫01背包呢?我的理解是01背包其实就是有没有的问题,所以01就代表了两种状态,取或不取。01背包适用于每种物品只有一件的情况,也是最简单的,但是会碰到各种奇怪的“WA法”,所以需要我们进行优化。
先看例题:
https://www.luogu.com.cn/problem/P2871https://www.luogu.com.cn/problem/P2871
题目描述
Bessie has gone to the mall's jewelry store and spies a charm bracelet. Of course, she'd like to fill it with the best charms possible from the N (1 ≤ N ≤ 3,402) available charms. Each charm i in the supplied list has a weight Wi (1 ≤ Wi ≤ 400), a 'desirability' factor Di (1 ≤ Di ≤ 100), and can be used at most once. Bessie can only support a charm bracelet whose weight is no more than M (1 ≤ M ≤ 12,880).
Given that weight limit as a constraint and a list of the charms with their weights and desirability rating, deduce the maximum possible sum of ratings.
输入格式
* Line 1: Two space-separated integers: N and M
* Lines 2..N+1: Line i+1 describes charm i with two space-separated integers: Wi and Di
输出格式
* Line 1: A single integer that is the greatest sum of charm desirabilities that can be achieved given the weight constraints
输入输出样例
输入 #1
4 6 1 4 2 6 3 12 2 7
输出 #1
23
从题目我们就不难看出需要我们进行决策,要在有限的空间中装价值最大的情况,其实就是拿还是不拿的问题,我们需要同时维护容量和价值两个约束条件,我们不妨开个二维数组,是一个关于物品数量和最大容量的数组。
在拿物品的时候,我们需要考虑两种情况:
一:当我们的下一个物品的重量超过了我们背包的极限时,我们无法将其带上,所以面对此物品是我们的决策应该于上一个物品是一样的,即dp[i][j] = dp[i - 1][j]。
二:当我们能带上某件物品时我们需要决策一下这件物品值不值得我们拿,因此,我们需要比较一下拿上它的价值和不拿的价值,也就是dp[i - 1][j]和dp[i - 1][j - w[i]] + v[i]哪个大我们就用其去更新数组。
因此,代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 13000;
const int M = 3410;
int w[N], v[N];
int dp[M][N];
int n ,m;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++)
cin >> w[i] >> v[i];
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
if(j < w[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
cout << dp[n][m] << endl;
}
当我们高高兴兴去交一发的时候,MLE了!看一下空间复杂度O(nm),确实有可能会MLE,那么如何改进呢?
相信有些同学看似会了,但是这个dp数组究竟是怎么操作的你真的明白吗?(其实一开始我也一直没弄懂,所以感觉总是学不进去)同时,想要优化空间,必须先弄懂dp数组的功能。
我们从i = 1开始,i代表了什么呢?i代表了行,第i行意味着当我们的背包里有i个东西时我们的决策,j是一项约束,是从1到我们背包的最大容量,一开始i = 1;j = 1;就是代表了当背包里只能有一个物品,同时容量最大为1时,我们背包里所能有的最大价值,接着j渐渐变大,也就是在受到物品限制1个的情况下容量渐渐变大,我们所能获得的最大价值,由于我们先前初始化初态均为0,我们遵循二循环的操作,不断通过dp[0]行进行更新。不断得到新的决策,保证了我们容量不断变大时记录下真正的最大价值。
而后,i ++,当背包物品约束变为两个时,容量依然从1开始慢慢变大,那么对于第二行来说,我们的基准态就是上一行记录下的dp[1]的价值,保证了我们的决策的正确性。
以此类推,我们不难看出,每次我们的基准态都是上一次循环的内容,而更早的内容就成了占位置的僵尸数据,因此我们想,那我们用一个一维数组存储基准态,下一次我们直接将其更新,不就不会有僵尸数据了吗?
理论可行,实践:
#include <bits/stdc++.h>
using namespace std;
const int N = 13000;
const int M = 3410;
int w[N], v[N];
int dp[N];
int n ,m;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++)
cin >> w[i] >> v[i];
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
if(j < w[i])
dp[j] = dp[j];
else
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
cout << dp[n][m] << endl;
}
但是红惨了,为什么呢?
我们发现,在更新新基准态的时候,我们是从前向后更新的,从而导致了有可能dp[j - w[i]]会被更新,但是我们后面的数据需要原来的基准数据,导致错误,既然从前往后不行,那么从后往前不就避免了这个问题了吗,因为后面的数据与前面不会有挂钩。
实践代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 13000;
const int M = 3410;
int w[N], v[N];
int dp[N];
int n ,m;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++)
cin >> w[i] >> v[i];
for(int i = 1; i <= n; i ++)
for(int j = m; j >= 1; j --)
{
if(j < w[i])
dp[j] = dp[j];
else
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
cout << dp[m] << endl;
}
这样就可以了!这就是逆序枚举的魅力。
关于滚动数组,其实这种一维数组也就是滚动数组,但从更直观的角度,我们可有开一个dp[2][m]的数组,这样就不怕新数据覆盖旧值的情况出现了。
其实上面的AC代码还能继续简化,我们可以将j < w[i]条件放到for中,从elsed语句的角度出发,就是防止j - w[i]出现负数的情况。
最简代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 13000;
const int M = 3410;
int w[N], v[N];
int dp[N];
int n ,m;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++)
cin >> w[i] >> v[i];
for(int i = 1; i <= n; i ++)
for(int j = m; j >= w[i]; j --)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
cout << dp[m] << endl;
}