01背包问题

题目

N N N件物品和一个容量为 V V V的背包。第i件物品的体积是 c [ i ] c[i] c[i],价值是 w [ i ] w[i] w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大,求出最大价值总和(要区别背包刚好装满和背包可以不装满的两种不同情况下的求解)。

基本思路

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

用子问题定义状态:即f[i][v]表示前i件物品放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
f [ i ] [ v ] = m a x ( f [ i − 1 ] [ v ] , f [ i − 1 ] [ v − c [ i ] ] + w [ i ] ) f[i][v]=max(f[i-1][v],\quad f[i-1][v-c[i]]+w[i]) f[i][v]=max(f[i1][v],f[i1][vc[i]]+w[i])

这里的 f [ i ] [ v ] f[i][v] f[i][v]定义是选取i个物品其背包容量加起来恰好为v。

这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。

  • 如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;
  • 如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为 v − c [ i ] v-c[i] vc[i]的背包中”,此时能获得的最大价值就是 f [ i − 1 ] [ v − c [ i ] ] f[i-1][v-c[i]] f[i1][vc[i]]再加上通过放入第i件物品获得的价值 w [ i ] w[i] w[i],即 f [ i ] [ v ] = f [ i − 1 ] [ v − c [ i ] ] + w [ i ] f[i][v]=f[i-1][v-c[i]]+w[i] f[i][v]=f[i1][vc[i]]+w[i]

初始值问题

1.要求背包刚好装满时的最大总价值时,此时参照状态转移方程的定义,从初始化的条件改变入手即可。

因为要求为背包恰好装满的情况下,所以初始为0时,即有0个物品被选时,只有 f [ 0 ] [ 0 ] = 0 f[0][0]=0 f[0][0]=0有意义,而 f [ 0 ] [ 1 ] f[0][1] f[0][1] f [ 0 ] [ 2 ] f[0][2] f[0][2] f [ 0 ] [ 3 ] f[0][ 3] f[0][3]…均无意义。因为题目要求的是恰好装满,所以在初始为0即没有选任何物品时, f [ 0 ] [ 1 ] f[0][1] f[0][1] f [ 0 ] [ 2 ] f[0][2] f[0][2] f [ 0 ] [ 3 ] f[0][ 3] f[0][3]…这些状态是不应该存在的,因为 f [ 0 ] [ 1 ] f[0][1] f[0][1]的定义为选取0个物品时,其背包容量加起来正好是1,同理, f [ 0 ] [ 2 ] f[0][2] f[0][2]的定义为选取0个物品时,其背包容量加起来正好是2…而这些都是不可能的,因为选择0个物品时,在 f [ i ] [ v ] f[i][v] f[i][v]的定义下,初始值除了 f [ 0 ] [ 0 ] f[0][0] f[0][0]有意义外, f [ 0 ] [ 1 ] f[0][1] f[0][1] f [ 0 ] [ 2 ] f[0][2] f[0][2] f [ 0 ] [ 3 ] f[0][ 3] f[0][3]…这些状态均无意义。
所以在要求背包刚好装满时的最大总价值时,初始设置应为 f [ 0 ] [ 0 ] = 0 f[0][0]=0 f[0][0]=0 f [ 0 ] [ 1 ] = f [ 0 ] [ 2 ] = f [ 0 ] [ 3 ] . . . f [ 0 ] [ v ] = − I N F f[0][1]=f[0][2]=f[0][3]...f[0][v]=-INF f[0][1]=f[0][2]=f[0][3]...f[0][v]=INF

2.要求背包允许不刚好装满时的最大总价值时,此时参照状态转移方程的定义,也可从初始化的条件改变入手。

因为在得到最大总价值时,背包可以不装满,则初始未选物品即0个物品被选时,除了 f [ 0 ] [ 0 ] f[0][0] f[0][0]有意义, f [ 0 ] [ 1 ] f[0][1] f[0][1] f [ 0 ] [ 2 ] f[0][2] f[0][2] f [ 0 ] [ 3 ] f[0][ 3] f[0][3]…均有意义,因为虽然 f [ 0 ] [ 1 ] f[0][1] f[0][1]的定义为选取0个物品时,其背包容量加起来正好是1,但由于此时背包可以不要求恰好装满等于1即可以少于1,即 f [ 0 ] [ 1 ] f[0][1] f[0][1] f [ 0 ] [ 2 ] f[0][2] f[0][2] f [ 0 ] [ 3 ] f[0][3] f[0][3] f [ 0 ] [ v ] f[0][v] f[0][v]的存在是有意义的。所以除了设置 f [ 0 ] [ 0 ] = 0 f[0][0]=0 f[0][0]=0外,初始化时还需 f [ 0 ] [ 1 ] = f [ 0 ] [ 2 ] = f [ 0 ] [ 3 ] . . . f [ 0 ] [ v ] = 0 f[0][1]=f[0][2]=f[0][3]...f[0][v]=0 f[0][1]=f[0][2]=f[0][3]...f[0][v]=0

假设有一个体积为10的背包,物品的体积分别为4、5、6,对应的价值为10,17,16 。
在这里插入图片描述
由上图可知,

  • 要求在背包v恰好装满时能达到的最大总价值
    则只有 f [ 0 ] [ 0 ] + 10 f[0][0]+10 f[0][0]+10 → \rightarrow f [ 1 ] [ 4 ] f[1][4] f[1][4] → \rightarrow f [ 2 ] [ 4 ] + 16 f[2][4]+16 f[2][4]+16 → \rightarrow f [ 3 ] [ 10 ] f[3][10] f[3][10]这样的一条路,但此时并不是最大总价值,只是说在满足背包恰好刚装满条件下所得的最大总价值。
  • 求背包v可以不恰好装满时能达到的最大总价值
    则可选择 f [ 0 ] [ 1 ] + 10 f[0][1]+10 f[0][1]+10 → \rightarrow f [ 1 ] [ 5 ] + 17 f[1][5]+17 f[1][5]+17 → \rightarrow f [ 2 ] [ 10 ] f[2][10] f[2][10] → \rightarrow f [ 3 , 10 ] f[3,10] f[3,10]这样的一条路,此时是最大总价值。

结果枚举问题

由上图分析可知,无论在要求恰好装满或者不要求恰好装满的条件下,最终得到的结果均不需要枚举,即cout<< f [ N ] [ V ] f[N][V] f[N][V]<<endl; 即可。

#include<iostream>
using namespace std;
#include<algorithm>

// 注意,若在函数内部则初始化是随机的,在全局上定义时默认初始化为 0
// 所以这里无需 c[105] = {0}...
int c[105], w[105];
int dp[105][1005];


int main(){

    int v, n;
    cin >> v >> n;
    for (int i = 1; i <= n; i++)
        cin >> c[i] >> w[i];
    for (int i = 1; i <= n; i++)
        for (int j = v; j >= 0; j--) {// 二维数组时,j使用顺序或逆序均可以
        	if(j >= c[i])
          		dp[i][j] = max(dp[i-1][j - c[i]] + w[i], dp[i-1][j]);
          	else
          		dp[i][j] = dp[i-1][j];
    }
    cout << dp[n][v] << endl;
    return 0;
}
/*
注意:
当设置为二维数组时,这里的第三个for循环不可写成
for(int i = 1; i <= n; i++)
	for(int j = v; j >= c[i]; j--)
		dp[i][j] = max(dp[i-1][j-c[i]] + w[i], dp[i-1][j]);

	因为若改成如上形式,
	则当j<c[i]时,dp[i][j]的内容得不到上一次dp[i-1][j]的更新;
	
但是在改为一维数组时,可以改成
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]);
	因为一维数组时,
	当则当j<c[i]时,dp[j]的内容默认为上一次状态的数组dp[j]保持不变。
*/

优化空间复杂度(使用一维数组)

以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。

f [ v ] = m a x ( f [ v ] , f [ v − c [ i ] ] + w [ i ] ) f[v]=max(f[v],\quad f[v-c[i]]+w[i]) f[v]=max(f[v],f[vc[i]]+w[i])恰就相当于我们的转移方程 f [ i ] [ v ] = m a x ( f [ i − 1 ] [ v ] , f [ i − 1 ] [ v − c [ i ] ] + w [ i ] ) f[i][v]=max(f[i-1][v],\quad f[i- 1][v-c[i]]+w[i]) f[i][v]=max(f[i1][v],f[i1][vc[i]]+w[i])
因为现在的 f [ v − c [ i ] ] f[v-c[i]] f[vc[i]]就相当于原来的 f [ i − 1 ] [ v − c [ i ] ] f[i-1][v-c[i]] f[i1][vc[i]]

  • 而一维数组时,j如果使用顺序即从小到大求,则因为后面的 f [ k ] f[k] f[k]使用到了前面的 f [ k − c [ i ] ] f[k-c[i]] f[kc[i]]可能已经被更新了,则相当于 f [ i ] [ k ] f[i][k] f[i][k] f [ i ] [ k − c [ i ] ] f[i][k-c[i]] f[i][kc[i]]更新,显然不对。
    ”由于v是从小到大,在求等式左边的f[v]前,某个 f [ k ] ( k < v ) f[k](k<v) f[k](k<v)就改了值了,而在求 f [ v ] f[v] f[v]时,这个 v − c [ i ] v-c[i] vc[i]可能又等于k,而这个k值之前被改过,即 f [ k ] f[k] f[k]已不是 i − 1 i-1 i1时刻的值,而是i时刻的值,但我们现在需要的是i-1时刻的值来求出i时刻的值,所以通过 f [ v − c [ i ] ] f[v-c[i]] f[vc[i]]求出的 f [ v ] f[v] f[v]值就是错误的值,与本题意不符合。“

  • 而逆序时,j是从v大到小, f [ v − c [ i ] ] f[v-c[i]] f[vc[i]]中的 v − c [ i ] v-c[i] vc[i]在此时肯定没变,因为是从后面往前面更新的,即 f [ v − c [ i ] ] f[v-c[i]] f[vc[i]]的更新也是排在 f [ v ] f[v] f[v]更新之后才进行更新的。

#include<iostream>
using namespace std;
#include<algorithm>

// 注意,若在函数内部则初始化是随机的,在全局上定义时默认初始化为 0
// 所以这里无需 c[105] = {0}...
int c[105], w[105];
int dp[1005];


int main(){

    int v, n;
    cin >> v >> n;
    for (int i = 1; i <= n; i++)
        cin >> c[i] >> w[i];
    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]);
    cout << dp[v] << endl;
    return 0;
}
/*
注意:
一维数组时,可以改成
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]);
	因为一维数组时,
	当则当j<c[i]时,dp[j]的内容默认为上一次状态的数组dp[j]不需要赋值改变。
*/

链接:

acwing讲解

01背包的两种初始化问题

dd_engi的背包九讲

背包九讲-整合版

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值