01背包问题
最近看了好多关于01背包问题的优化,没找到一篇通俗易懂的,大概是我太笨了,所以根据自己的理解简单写一写,(如有错误,欢迎大牛指出)
0-1 背包问题:给定 N 种物品和一个容量为 V 的背包,第 i 个 物品 的重量是 wi,其价值为 vi 。
问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
01背包,0和1 就是表示在面对一种决策( 这里是指选择拿不拿物品的一种决策 )的时候,可以选择拿物品 i ,也可以不拿物品 i ;
它的 状态转移方程为 :
什么意思呢?
表示的是 只看前 i 个物品 ,当背包最大可以装载的体积为 j 时的最大价值 , 那么它是由我们决策之后的出来的
也就是说,当我们面对第 i 个物品时 (此时我们背包里已经放了 i-1 个物品),我们考虑到底放不放第i个物品? 如果放,会不会使得价值增大 .
显然, 如果不放第 i 个物品 ,那么 背包里不就是还是 i-1 个物品么,当然最大可装载体积为 j ,价值方程为 :
; 如果选择放第 i 个物品 ,那么 背包的价值可能会增大(前提是背包能装下第i 个物品) 所以
的意思就是在背包中 腾出 v[i] 个体积来放第i 的物品 . 所以价值方程为 :
. 我们考虑最大价值 :
题目
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤10000<N,V≤1000
0<vi,wi≤10000<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std ;
const int MAX = 1005 ;
int dp[MAX][MAX] ;
int w[MAX],v[MAX] ;
int main()
{
int N , V ;
scanf("%d%d",&N,&V);
for(int i = 1 ; i<=N ;i++)
{
scanf("%d%d",&v[i],&w[i]);
}
for(int i = 1 ;i<=N ; i++)
{
for(int j = 0; j<=V ;j++)
{
if(j>=v[i])
{
dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
}
else
{
dp[i][j] = dp[i-1][j] ;
}
}
}
printf("%d\n",dp[N][V]);
return 0 ;
}
其实我们可以对01背包进行优化 , 当然是空间上的优化,我们用滚动数组 , 因为通过 状态方程 我们可以看出dp[i][j] 它只和dp[i-1][j]的状态有关 ,换句话说 dp[i][j] 是 由 它上一个状态转移来的 .
由图表结合转移方程可以看出 , 比如 第2 行 第 2列的 4 是由 第1 行的 第2 列 和 第1 行的第1 列转移来的 ,所以我们只要保留第i -1 行的数据 和第 i 行的数据即可 .
得到
, 不理解没关系, dp[j] 表示的是背包最大装载的体积为j 时的最大体积 ,我们这个方程是根据二维数组的状态方程写出来的 , 它们其实是一一对应的 状态转移方程右边的 dp[j] 就是 i-1 状态下的值, 左边是 i 状态面对决策的状态量,它还没更新哦 ,一定要注意 (等式两边的 dp[j] 代表的不一样 ,我在强调一遍 ,左边是 i 状态下的值,它还没更新 ,而右边的dp 是 i -1 状态的值 ,它已经更新过了 ,这样也就可以解释 i-1 状态向 i 状态 转移 ), 缓一缓 .
重点来了 , 在二维数组的代码中 我们第二层循环 是从 0 开始枚举背包容量 ,但是在一维下 要改成 从 V 递减枚举 到 j >=v[i] ,这是为什么,其实我们可以打个表 ,
4 5
1 2
2 4
3 4
4 5
dp[ 5 ] : 0 | dp[ 5 - 1 ] : 2 ** dp[ 5 ] : 2 | dp[ 5 - 1 ] : 2
dp[ 4 ] : 0 | dp[ 4 - 1 ] : 2 ** dp[ 4 ] : 2 | dp[ 4 - 1 ] : 2
dp[ 3 ] : 0 | dp[ 3 - 1 ] : 2 ** dp[ 3 ] : 2 | dp[ 3 - 1 ] : 2
dp[ 2 ] : 0 | dp[ 2 - 1 ] : 2 ** dp[ 2 ] : 2 | dp[ 2 - 1 ] : 2
dp[ 1 ] : 0 | dp[ 1 - 1 ] : 2 ** dp[ 1 ] : 2 | dp[ 1 - 1 ] : 2
dp[ 5 ] : 2 | dp[ 5 - 2 ] : 6 ** dp[ 5 ] : 6 | dp[ 5 - 2 ] : 6
dp[ 4 ] : 2 | dp[ 4 - 2 ] : 6 ** dp[ 4 ] : 6 | dp[ 4 - 2 ] : 6
dp[ 3 ] : 2 | dp[ 3 - 2 ] : 6 ** dp[ 3 ] : 6 | dp[ 3 - 2 ] : 6
dp[ 2 ] : 2 | dp[ 2 - 2 ] : 4 ** dp[ 2 ] : 4 | dp[ 2 - 2 ] : 4
dp[ 5 ] : 6 | dp[ 5 - 3 ] : 8 ** dp[ 5 ] : 8 | dp[ 5 - 3 ] : 8
dp[ 4 ] : 6 | dp[ 4 - 3 ] : 6 ** dp[ 4 ] : 6 | dp[ 4 - 3 ] : 6
dp[ 3 ] : 6 | dp[ 3 - 3 ] : 4 ** dp[ 3 ] : 6 | dp[ 3 - 3 ] : 4
dp[ 5 ] : 8 | dp[ 5 - 4 ] : 7 ** dp[ 5 ] : 8 | dp[ 5 - 4 ] : 7
dp[ 4 ] : 6 | dp[ 4 - 4 ] : 5 ** dp[ 4 ] : 6 | dp[ 4 - 4 ] : 5
8
我这里 " | " 符合前后是 更新dp[j] 前的值 , "**" 是 更新dp[j] 后的值 ,
我再手写一下哈 , 如果从 v[i] 到 V 递增枚举 ,(这里 V 代表 背包总容量)
比如 在外层循环到 2 的时候 ( i= 2 ) ,
v[i] = 2 ; (看excel表上的也行)
j = 2 , dp[2] = max(dp[2] ,dp[0]+ w[2] ) , 其中等式 右边的dp[2] 表示 i-1 状态下的
j = 3 , dp[3] = max(dp[3] ,dp[1] + w[2] ) ,
j = 4 , dp[4] = max(dp[4] ,dp[2] + w[2]) ,发现什么了吗 , dp[2] 刚刚更新完,是第 i 状态下的, 所以 dp[4] 就不是从 第 i -1 状态下转移的,所以错在这 .
正确的是 , 枚举V 到 v[i]
例题代码:
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std ;
const int N = 1005 ;
int dp[N] ;
int w[N] ,v[N] ;
int main()
{
int N , V ;
scanf("%d%d",&N,&V);
for(int i = 1 ;i<=N ;i++)
{
scanf("%d%d",&v[i],&w[i]);
}
for(int i = 1 ;i<= N ;i++)
{
for(int j = V ;j>=v[i] ;j--)
{
dp[j] = max(dp[j],dp[j-v[i]]+w[i]) ;
}
}
printf("%d",dp[V]);
return 0 ;
}