背包问题

背包问题

  1. 01背包 每件物品最多只用一次
  2. 完全背包 每件物品有无限个
  3. 多重背包 每个物品最多有si个(朴素版,优化版)
  4. 分组背包,有n组,每组物品有若干种

简化的01背包

分析:

  • 原问题:i件物品选若干件组成的小于V的最大体积是多少?
  • 用可行性描述就可
  • bool数组f[i][j]表示前i个物品能否放满体积为j的背包
  • 枚举最后一次决策——第i个物品放还是不放
  • f[i][j]=f[i1][j]||f[i1][ja[i]]
  • 初值 f[i][j]=0,f[0][0]=1

image

  • 我们可以看到每一行的结果实际上只与上一行有关,所以就可以01滚动——f[0,1][j]一行记录前一行的值,另一行记录当前行的值
  • 对于本题更加常用的方法是就地滚动
  • 就地滚动就是用一个一维数组,之前的状态和当前的状态都记在同一个数组里了
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int v, n;
int a[40];
int f[2][20020];
int main() {
cin >> v >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
memset(f, 0, sizeof(f));
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= v; j++) { //这里j要从0开始,不能从a[i]
if (j >= a[i]) {
f[i % 2][j] = f[(i - 1) % 2][j] || f[(i - 1) % 2][j - a[i]];//放或不放
} else {
f[i % 2][j] = f[(i - 1) % 2][j]; //小于就直接继承
}
}
}
int ans = 0;
for(int i = v; i >= 0; i--){
if(f[n%2][i] == 1){
ans = i;
break;
}
}
cout << v - ans << endl;
return 0;
}
/*
输入:24 6
8 3 12 7 9 7
输出:0
*/

01背包

题目描述:
N件物品和一个容量是V的背包。每件物品只能使用一次。
第i件物品的体积是vi价值是wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

分析:
动态规划是不断决策求最优解的过程,「0-1 背包」即是不断对第i
个物品的做出决策,「0-1」正好代表不选与选两种决定。

题解代码

version 1递归

最朴素的方法,针对每个物品是否放入背包进行搜索

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010;
int n, m;
int w[N], v[N];
//从第i个物品开始挑选总量小于j的部分
int rec(int i, int j) {
int res;
if (i == n) { //已经没有剩余物品
res = 0;
} else if (j < w[i]) { //无法挑选这个物品
res = rec(i + 1, j);
} else {
//挑选和不挑选的两种情况都尝试一下
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
}
return res;
}
void solve() {
printf("%d\n", rec(0, m));
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
solve();
return 0;
}

这种方法的搜索深度是n,而且每一层的搜索都需要两次分支,最坏就需要O(2n)的时间,n较大无法求解。
image
如图,rec以(3,2)为参数调用了两次。第二次调用已经知道了结果却浪费了时间。我们可以在这里把第一次计算的结果记录下来。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010;
int n, m;
int w[N], v[N];
int dp[N][N]; //记忆化数组
//从第i个物品开始挑选总量小于j的部分
int rec(int i, int j) {
//如果已经计算过的话直接使用之前的结果
if (dp[i][j] >= 0) return dp[i][j];
int res;
if (i == n) { //已经没有剩余物品
res = 0;
} else if (j < w[i]) { //无法挑选这个物品
res = rec(i + 1, j);
} else {
//挑选和不挑选的两种情况都尝试一下
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
}
return dp[i][j] = res;
}
void solve() {
//用-1表示尚未计算过,初始化整个数组
memset(dp, -1, sizeof(dp));
printf("%d\n", rec(0, m));
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
solve();
return 0;
}

这个优化对于同样的参数,只会在第一次被调用时执行递归部分,第二次之后都会直接返回。这种方法就是记忆化搜索。

version 2 二维

(1)状态f[i][j]定义前i个物品,背包容量j下的最优解(最大价值);

  • 当前的状态依赖于之前的状态,可以理解为从初始状态f[0][0]=0,开始决策,有n件物品,则需要n次决策,每一次对第i件物品的决策,状态f[i][j]不断由之前的状态更新而来。
    (2)当前背包容量不够(j<v[i]),没得选,因此前i个物品最优解即为前i1个物品最优解。
  • 对应代码:f[i][j]=f[i-1][j];
    (3)当前背包容量够,可以选,因此需要决策选与不选第i个物品:
  • 选:f[i][j]=f[i-1][j-v[i]]+w[i];
  • 不选:f[i][j]=f[i-1][j];
  • 我们的决策是如何取到最大价值,因此以上两种情况取max()
    image
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {//01背包 二维 正序/逆序更新都可以,完全背包二维只能正序更新
if (j < v[i]) f[i][j] = f[i - 1][j];
else f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
//完全背包:f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i];
}
}
cout << f[n][m] << endl;
return 0;
}

version 3 一维

将状态f[i][j]优化到一维f[j],实际上只需要做一个等价变形。
为什么可以?
我们定义的状态f[i][j]可以求得任意合法的ij最优解,但题目只需要求得最终状态f[n][m],因此只需要一维的空间来更新状态。
(1)状态f[j]定义:N件物品,背包容量j下的最优解。
(2)注意枚举背包容量j必须从m开始
(3)为什么一维情况下枚举背包容量需要逆序? 在二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]f[i - 1][j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。
(4)例如,一维状态第i轮对体积为3的物品进行决策,则f[7]f[4]更新而来,这里的f[4]正确应该是f[i - 1][4],但从小到大枚举j这里的f[4]在第i轮计算却变成了f[i][4]。当逆序枚举背包容量j时,我们求f[7]同样由f[4]更新,但由于是逆序,这里的f[4]还没有在第i轮计算,所以此时实际计算的f[4]仍然是f[i - 1][4]
状态转移方程:f[j] = max(f[j], f[j-v[i]] + w[i]);
1.如果当前位置的东西不拿的话,和前一位置的信息(原来i-1数组的这个位置上的值)是相同的,所以不用改变。
2.如果当前位置的东西拿了的话,需要和前一位置的信息(原来i-1数组的这个位置上值)取max。
3.每次i++,就从后往前覆盖一遍f数组,看每个位置上的值是否更新。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++) {
for (int j = m; j >= v[i]; j--) { //01背包 二维--> 一维后只能逆序更新
//for(int j = 0; j <= m; j++) //01背包二维更新,正序和逆序都可以
if (j < v[i]) f[j] = f[j]; //j < v[i],f[j] = f[j]是恒等式可以删除
//f[i][j] = f[i-1][j]; //01背包(二维)
else f[j] = max(f[j], f[j - v[i]] + w[i]);
// 01背包(二维): f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
// 完全背包(二维):f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
}
}
cout << f[m] << endl; //f[n][m] --> f[m]
return 0;
}

实际上,只有当枚举的背包容量>= v[i]时才会更新状态,因此我们可以修改循环终止条件进一步优化。

关于状态f[j]的补充说明
二维下的状态定义是前i件物品,背包容量j下的最大价值,一维下,少了前i件物品这个维度,我们的代码中决策到第i件物品(循环到第i轮),f[j]就是前i轮已经决策的物品背包容量j下的最大价值。
因此当执行完循环结构后,由于已经决策了所有物品,f[j]就是所有物品背包容量j下的最大价值。即一维f[j]等价于二维f[n][j];

完全背包

朴素算法(数据加强,已tle)

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e3 + 10;
int n, m;
int v[N], w[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
for (int k = 0; k * v[i] <= j; k++) {
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}

image

实际上,我们在计算状态方程时不必多一个循环去单独枚举选择第i个物品个数。
二维朴素写法

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
f[i][j] = f[i - 1][j];
if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
}
cout << f[n][m] << endl;
return 0;
}
// 完全背包:二维朴素写法
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main(){
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ ){ // 完全背包 二维 只能 正序更新, 01背包 二维 正序/逆序 更新 都可以
// for (int j = m; j >= 0; j -- ){ // 完全背包 二维 逆序更新 会报错
if (j < v[i]) f[i][j] = f[i - 1][j];
else f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
// 01 背包:f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
  • 完全背包二维之所以只能正序更新,不能逆序更新是因为:f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);想求f[i][j-v[i]],两者都是f[i],也就是在同一层,所以只能正序更新。
  • 01背包二维之所以正序逆序都可以是因为:f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);想求f[i][j],要先求f[i-1][j-v[i]],前者是f[i],后者是f[i-1],不在同一层,所以正序逆序更新都可以。
    优化空间到一维
// 完全背包:二维朴素写法 ---> 一维空间优化写法 过程展示:
#include<iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main(){
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ ){ // 完全背包 一维 只能 正序更新
// 01背包 一维 只能 逆序更新: for (int j = m; j >= v[i]; j -- )
if (j < v[i]) f[j] = f[j];
// 完全背包(二维):f[i][j] = f[i - 1][j];
else f[j] = max(f[j], f[j - v[i]] + w[i]);
// 完全背包(二维):f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
// 01 背包(二维): f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[m] << endl; // f[n][m] ---> f[m]
return 0;
}

完全背包:二维朴素写法 —> 一维空间优化写法

  • 完全背包:一维空间优化写法, 将 以上代码 最终简写为如下:( 注意 for (int j = v[i]; j <= m; j ++ ) j 初始化为 v[j],简化之前 j 初始化为 0
// 完全背包:一维空间优化写法, 将 以上代码 最终简写为如下:
// 注意 for (int j = v[i]; j <= m; j ++ ) 中 j 初始化为 v[j],简化之前 j 初始化为 0
#include<iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main(){
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++ )
for (int j = v[i]; j <= m; j ++ ) // 完全背包 一维 只能 正序更新
// 01背包 一维 只能 逆序更新: for (int j = m; j >= v[i]; j -- )
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl; // f[n][m] ---> f[m]
return 0;
}

多重背包问题1

朴素写法

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 110;
int n, m;
int v[N], w[N], s[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i] >> s[i];
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= s[i] && k * v[i] <= j; k++) {
f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
}
}
}
cout << f[n][m] << endl;
return 0;
}

二进制优化写法

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 25000, M = 2010;
int n, m;
int v[N], w[N];//逐一枚举最大是N*logS
int f[N]; //体积< M
int main() {
cin >> n >> m;
int cnt = 0;//分组的组别
for (int i = 1; i <= n; i++) {
int a, b, s;
cin >> a >> b >> s;
int k = 1;//组别里面的个数
while (k <= s) {
cnt++;//组别先增加
v[cnt] = a * k;//整体体积
w[cnt] = b * k;//整体价值
s -= k;//s要减小
k *= 2;//组别里的个数增加
}
//剩余的一组
if (s > 0) {
cnt++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt;//枚举次数正式由个数变成组别数
//01背包一维优化
for (int i = 1; i <= n; i++) {
for (int j = m; j >= v[i]; j--) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}

佬的题解

https://www.acwing.com/problem/content/discussion/content/2807/
多重背包二进制优化题解

posted @   csai_H  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示