废话一堆
(本篇着重阐述的是笔者不熟的,目的也是为了帮助笔者理清知识,所以有很多不够详细的地方,望多多担待)(总而言之就是本篇是笔者写给自己看的)
背包应该是今年疫情前学的吧,笔者在复习的时候才发现自己的背包学得有多么辣鸡。所以笔者只能比学得好的大佬多用时间好好的整理一下自己的背包(欢迎大佬提意见,但是求轻喷)
前言
度娘说:背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高
用辣鸡笔者的话转换一下就是:在有限容量时面对不同情况,能获得的最高价值。
那么在这些不同的情况中,最基础的就是01背包,完全背包,以及多重背包。
01背包
gm的ppt说:有N件物品和一个容量为V的背包。放入第i件物品耗费的体积是vi,得到的价值是Pi。
求解:将哪些物品装入背包可使价值总和最大?
总而言之:每一种物品有且只有一件,故只有选与不选两种,所以称作01背包。
一维数组优化
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 35;
int m, n;
int w[maxn], c[maxn], dp[maxn];
int main() {
scanf("%d %d", &m, &n);
for(int i = 1; i <= n; i ++){
scanf("%d %d", &w[i], &c[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]] + c[i]);
}
}
printf("%d", dp[m]);
return 0;
}
经典例题(版题)有:采药,开心的金明,Subset Sums集合…
完全背包
完全背包与01背包不同的是完全背包中的物品有无限件
\\朴素版
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1e5;
int m, n;
int a[maxn], b[maxn];
long long dp[maxn];
int main() {
scanf("%d %d", &m, &n);
for(int i = 1; i <= n; i ++){
scanf("%d %d", &a[i], &b[i]);
}
for(int i = 1; i <= n; i ++){
for(int j = m; j >= a[i]; j --){
for(int k = 1; k <= j / a[i]; k ++){
dp[j] = max(dp[j], dp[j - k * a[i]] + k * b[i]);
}
}
}
printf("%d", dp[m]);
return 0;
}
从这上面,我们就可以发现朴素算法的弱点了,它的时间复杂度是 O ( n 3 ) O( n^3 ) O(n3)一不小心就超时
如果我们把它转化为01背包来思考呢?一件东西如果我们要取它,就必须保证它取了至少一件,因此我们转化为先放一件进背包然后再考虑下一个是否取的问题。
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1e5;
int m, n;
int a[maxn], b[maxn];
long long dp[maxn];
int main() {
scanf("%d %d", &m, &n);
for(int i = 1; i <= n; i ++){
scanf("%d %d", &a[i], &b[i]);
}
for(int i = 1; i <= n; i ++){
for(int j = a[i]; j <= m; j ++){
dp[j] = max(dp[j], dp[j - a[i]] + b[i]);
}
}
printf("%lld", dp[m]);
return 0;
}
多重背包
多重背包其实与01背包没有本质性的区别,只是完全背包每一件东西都是有限件但可以不止一件。
//依旧是朴素
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 20;
int n, m;
int a[maxn], b[maxn], num[maxn];
long long dp[maxn * maxn];
int main() {
scanf("%d %d", &m, &n);
for(int i = 1; i <= n; i ++){
scanf("%d %d %d", &a[i], &b[i], &num[i]);
}
for(int i = 1; i <= n; i ++){
for(int j = m; j >= 1; j --){
for(int k = 1; k <= num[i] && k <= j / a[i]; k ++){
dp[j] = max(dp[j], dp[j - a[i] * k] + k * b[i]);
}
}
}
printf("%lld", dp[m]);
return 0;
}
朴素依旧时间复杂度极高
这是我们就要引出大名鼎鼎的二进制优化啦(金色传说)
又是
gm的ppt说:
二进制优化思想:每类物品都有 n u m i num_i numi件,而01背包DP的思想是每件物品取与不取的线性递推,也就是说每类物品可以取0,1,2,……, n u m i num_i numi 件。而我们使用二进制把原有的 n u m i num_i numi件相同物品合并成体积和价值都扩大系数倍的1,2,4,8,……, numi -2k+1 件不同的物品,同样可以通过取与不取组合出0,1,2,……, n u m i num_i numi件相同物品的效果,且组合之后的每件不同物品都只能取到1次。这样处理之后,时间复杂度变为 O ( l o g 2 ∑ n u m i ∗ V ) O(log2∑numi*V) O(log2∑numi∗V) , 1<=i<=n
我们知道,所有自然数都可以用二进制来表示,因此每一件东西的数量我们都转化成可以由0,1,2,4…组成的。
// 二进制优化版
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 205;
int m, n;
int v[maxn], p[maxn], dp[maxn];
int cnt;
int main() {
scanf("%d %d", &m, &n);
for(int i = 1; i <= n; i ++){
int one_v, one_p, one_num;
scanf("%d %d %d", &one_v, &one_p, &one_num);
for(int j = 1; j <= one_num; j <<= 1){
cnt ++;
v[cnt] = j * one_v;
p[cnt] = j * one_p;
one_num -= j;
}
if(one_num){
cnt ++;
v[cnt] = one_num * one_v;
p[cnt] = one_num * one_p;
}
}
for(int i = 1; i <= cnt; i ++){
for(int j = m; j >= v[i]; j --){
dp[j] = max(dp[j], dp[j - v[i]] + p[i]);
}
}
printf("%d", dp[m]);
}
总结
01,完全,多重背包都是最基础,必须要掌握的背包。同时最重要的还是01背包,因为任何的背包都可以转换为01背包解决。
笔者花了很长很长的时间终于写完了本篇,大概对于这三种背包的理解也更深一步了吧~
同时背包一共有九讲,对于混合背包,二维费用背包就下次再写吧!
下次一定
特备鸣谢
郭老师和他(她)的PPT,
大佬关于完全背包的博客