1 01背包问题
前言:
文章部分选自学长博客
与 dd_engi的背包九讲 这个你们随便一搜就能搜到
刚写完就查出自己写的错误:
(j> cost[i])不是(w[i])
1.1 题目
- 有 (N) 件物品和一个容量为(V) 的背包。放入第 (i) 件物品耗费的费用是 (C_i) ,得到的
价值是 (W_i) 。求解将哪些物品装入背包可使价值总和最大。
1.2基本思路
- 有(N) 种物品每种仅有一件那么问题就拆分成
- 把前(i)件物品放进容量为(v)的背包中最大价值为多少
- 那第(i)件物品放进去的最大价值是由(i-1)转移过来的,由此可以得到转移方程
转移方程
(i)第几个物品 j背包所剩余的空间
(dp[i][j] = max( dp[i-1][j],dp[i-1][j+c[i]]+w[i]))
Code
for(int i = 1 ; i <= n ;i++){
for(int j = v ; j >= 0; j--){
if(j >= c[i]) dp[i][j] = max(dp[i-1][j-c[i]]+w[i],dp[i-1][j]);
}
}
1.3优化空间复杂度
时间复杂度 (O(VN)) 空间复杂度 (O(VN))
但是空间复杂度是可以优化的可以优化为(O(V))
- 已经可以确定(dp[i][j])是最优解了,那么很显然可以推得
(dp[j] = max(dp[j],dp[v-c[i]]+w[i]))
Code
for(int i = 1 ; i <= n ;i++){
for(int j = v ; j >= c[i]; j--){
dp[j] = max(dp[j-c[i]]+w[i],dp[j]);
}
}
1.4 初始化细节
两类问题
- 有的题目要求"恰好装满背包"时的最优解
- 有的题目则并没有要求必须把背包装满
- 对于1: 要求恰好装满背包,那么在初始化时除了(dp[0])为0其它
(dp[1...V])均设为(-inf)这样就可以保证最终得到的(dp[N])是一种恰好装满背包的最优解。 - 对于2: 没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将(dp[0...V])全部设为0。
解释:
精简版本:如果要求恰好装满:那么dp[i] = 0 是不合法的 ,如果对装满没有要求,那么 dp[i] = 0就是合法的
原文:
这是为什么呢?可以这样理解:初始化的 dp 数组事实上就是在没有任何物品可以放
入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可以在什
么也不装且价值为 0 的情况下被“恰好装满” ,其它容量的背包均没有合法的解,属于
未定义的状态,应该被赋值为 -∞ 了。如果背包并非必须被装满,那么任何容量的背包
都有一个合法解“什么都不装” ,这个解的价值为 0 ,所以初始时状态的值也就全部为 0
了。
1.5 常数的优化
前缀和优化被学长喷了,好后缀和优化
前面的代码中有for(j=V...c[i])还可以将这个循环的下限进行改进。
由于只需要最后(dp[j])的值,倒推前一个物品,其实只要知道
dp[j-c[n]]即可。以此类推,对以第j个背包,其实只需要知道到dp[j--sum(c[j...n])]即可,即代码可以改成
Code
for(int i = 1 ; i <= n ;i++){
c[i] = read();
sum[i] = sum[i - 1] + c[i];
}
for (int i = 1; i <= n; i++) {
int bound = max(V - (sum[n]-sum[i-1]), c[i]);
for (int j = V; j >= bound, j--)
dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
}
这种优化适用于V较大时---典型的空间换时间的方式
1.6 小结
- 01 背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思
想。另外,别的类型的背包问题往往也可以转换成 01 背包问题求解。故一定要仔细体
会上面基本思路的得出方法,状态转移方程的意义,以及空间复杂度怎样被优化。
2 完全背包
2.1 题目
有 (N) 种物品和一个容量为(V) 的背包,每种物品都有无限件可用。放入第(i)种物品
的费用是 (C_i),价值是(W_i)。求解:将哪些物品装入背包,可使这些物品的耗费的费用总
和不超过背包容量,且价值总和最大。
2.2 基本思路
01背包考虑的是取与不取当前物品,而完全背包则是转换成了取几件的问题从中可以得到一个类似于01背包的转移方程
(dp[i][j]max(dp[i-1][j-k*c[i]]+k*w[i],dp[i-1][j]) | 0 <= k*w[i] <= j)
Code (又是口胡的,不知正确性,没人写这个没用的代码,希望有人能指出我的错误)
for(int i = 1; i <= n ;i++ ){
for(int j = c[i] ; j <= v ;j++){
for(int k = 1 ; k <= v/c[i] ;k++)
dp[i][j] = max(dp[i-1][j], dp[i-1][j - k * c[i]] + w[i] * k);
}
}
2.3简单优化
完全背包问题有一个很简单有效的优化,是这样的:若两件物品(i),(j)满足(c[i]<=c[j])且(v[i]>=v[j]),则将物品(j)去掉,
不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j jj换成物美价廉的i ii,得到至少不会更差的方案。
对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,
因为有可能特别设计的数据可以一件物品也去不掉。
这个优化可以简单的 (O(N^2)) 地实现,一般都可以承受。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于(V)的物品去掉,
然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以(O(V+N))地完成这个优化。
2.4转化为01背包的问题
第(i)件物品最多选(V/C_i)件于是,第(i)件物品就变成了(V/C_i)件(cost)为(c[i]),收益为(W[i])的物品
然后按01背包处理即可
看不懂的二进制优化/kk
更高效的转化方法是:把第(i)种物品拆成费用为(w[i]*2^k)价值为(v[i]*2^k)的若干件物品,其中(k)满足(w[i]*2^k<=V)这是二进制的思想,因为不管最优策略选几件第(i)种物品,
总可以表示成若干个(2^k)件物品的和。这样把每种物品拆成(O(log(V/w[i])))件物品,是一个很大的改进。但我们有更优的(O(VN))的算法。
2.5(The Code)
扯一句:对照01背包,这时候就会明白为什么01背包是倒序,而完全背包是正序完全背包每个物品可以选无数次,从前往后选就可以,而01背包每种物品只能选一次那就倒着选呗
纠正一下自己含糊不清的表达,回到最初的转移方程中
(dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]]+w[i] ))
线性dp要符合用第(i-1)个阶段向第(i)个阶段进行转移,随着(j)的不断缩小,也就满足了这个条件,从而保证第(i)个物品只能进入一次
用不是人话的话说一句:倒序是把状态(i)先由状态(i-1)转移过来,然后才将(i-1)状态转移为(i),正序是状态(i)被之前的状态(i-x)更新了数次,比方状态(i)被状态(i-1)更新又被状态(i-2)又更新一遍,差不多就是这么个意思(被学长教训后,老老实实重新学了一遍,然后并解释不清楚)
(O(VN))
for(int i = 1 ; i <= n ;i++){
for(int j = c[i] ; j <= v ;j++){
dp[j] = max(dp[j],dp[j - c[i]] + w[i] );
}
}
转化出的DP方程
(dp[i][j]=max(dp[i-1][j],f[i][j-c[i]]+w[i]))
2.6小结
完全背包问题也是一个相当基础的背包问题,它有两个状态转移方程,分别在“基本思路”以及“(O(VN))的算法“的小节中给出。希望你能够对这两个状态转移方程都仔细地体会,不仅记住,也要弄明白它们是怎么得出来的,最好能够自己想一种得到这些方程的方法。事实上,对每一道动态规划题目都思考其方程的意义以及如何得来,是加深对动态规划的理解、提高动态规划功力的好方法。
后记
一个莫名其妙RE的题目
在跑完全背包时候会发现这个背包的空间在逐渐扩大,也就是说一开始开数组时要把可能出现的最大值也开出来,这个题数据不是特别毒瘤5e6就能过了
#include<iostream>
#include<cstdio>
#include<cmath>
#define int long long
using namespace std;
const int N = 50+10;
const int V = 5e6+10;
int c[N] ,w[N] ,dp[V];
main() {
int s, n, d;
scanf("%lld%lld%lld",&s,&n,&d);
for(int i = 1 ; i <= d ; i++) {
scanf("%lld%lld",&c[i],&w[i]);
}
for(int k = 1 ; k <= n ;k++){
for(int i = 1 ; i <= d ;i++){
for(int j = c[i] ; j <= s ;j++ ){
dp[j] = max(dp[j],dp[j-c[i]]+w[i]);
}
}
s += dp[s];
}
printf("%lld",s);
return 0;
}
3 多重背包
3.1 题目
有 (N) 种物品和一个容量为 (V) 的背包。第 (i) 种物品最多有 (M_i) 件可用,每件耗费的
空间是 (C_i) ,价值是 (W_i) 。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
3.2 基本算法
- 按照 01 背包是思想把这(sum_{i=1}^{i<=N}M[i])件物品每一件都当成一件物品,然后01背包处理,但是很显然,这是一个(N^3)的暴力,
估计出题人不会这么友好所以大部分是要用二进制拆分来优化多重背包,单调队列优化01背包问题,暂时还没有弄明白,考完CSP2020再来做笔记,这里暂时只记录二进制优化的。
二进制优化就是由每一个数都可以由二进制拆分可以得到,那么这时候就有点倍增内味了, 倍增是每次跳(2^i)层,那二进制就是把它可以选的个数拆分,用(2^k)表示有几个可以选,简单就是优化了第2层循环(每一种物品能选的数量)
没有优化前的Code
for(int i = 1 ; i <= n ; i++){
for(int k = 1 ; k <= num[i] ; j++){
for(int j = V ; j >= c[i] ; j--){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-c[i] * k] +w[i] * k);
}
}
}
二进制优化后的
for(int i = 1 ; i <= n ;i++){
int num = min(numb[i],V/c[i]);
for(int k = 1 ; num > 0 ; k >>= 1){
if(k > num) k = num ;
num -= k;
for(int j = V ;j >= c[i] l j--){
dp[j] = max(dp[j}, dp[j - c[i] * k] + w[i] * k);
}
}
}
理解如有错误请指出
单调队列优化(占个位置):