动态规划:01背包问题的浅谈

经典01背包问题:

(1)定义:

你有n种物品,每种只有一个,每个物品有一个价值c和重量w。有一个背包,最多能装总重量为w的物品,求能装进背包物品的最价值最大值。

(2)解析:

首先不妨想另外一个问题:如果把物品换成“沙子”一类的东西,即可以取任意实数范围内的个数呢?不难想到一定要取“单位价值”最大的东西,即c/w最大,因为最后总能填满背包。

然而对于整数个范围的个数,这样的贪心方法是否适用呢?很遗憾,答案是否。不难想出这样一个反例:

只有两种物品,一种c=100,w=49,另一种c=20,w=11,背包最大重量为97。按照上面的贪心策略,100/49>2>20/11,所以该选第一种。然而第一种只能放一个,总价值为100;然而第二种却能放8个,总价值为160>100

问题来了:这是为什么呢?
很简单,因为这里的策略是考虑“沙子”的:算法认为接下来第一种物品还能再放0.93个,实际上却不行:你没法放半个物品。

那如果按价值,或是重量贪心呢?也很容易举出反例。

(3)算法:

这里,我们的优化利器:动态规划(dynamic programming)就登场了。

我们用二维数组保持状态:F[i,j],表示“前i个物品放到一个最大载重为j的背包,物品最大的价值和”。根据状态写出一个填表的二维循环:

for i=1 to n
for j=w[i] to v
{………………}

动态转移? 在选择第i个物品时,就两种情况:选,还是不选。这样问题就成了“前i-1个物品的问题”,不难得出:

选:F[i-1,j-w[i]]+c[i] : 前i-1个物品,容量为j-w[i]的背包(容量被i号物品占了w[i])
不选:F[i-1,j] : 前i-1个物品,容量为j的背包(背包没小)

得到方程:F[i,j]=max(F[i-1,j],F[i-1,j-w[i]]+c[i])
初始状态:F[0,j]=0 (0<=j<=v)
目标状态:F[n,v]

(4)空间的优化:

不难看出,时间复杂度O(nv)似乎没法优化了(否则表都填不完了),那空间呢?
这里就有了典型的动归优化:滚动数组。

请先仔细观察方程:F[i,j]=max(F[i-1,j],F[i-1,j-w[i]]+c[i])

我们得到一个结论:一个状态(i,j),它只从(i-1)层的状态转移,也就是说,当我们计算到第i层,1~i-2层都是没用的。那我们干嘛留着它呢?

为此,我们只用一个一维数组F[j]保存状态。但还有个问题:二维循环要写成这样:

for i=1 to n
for j=v to w[i]
F[j]=max(F[j],F[j-w[i]]+c[i])

这又是为什么呢?

当我们计算F[j],我们想知道F[1~j-1]的值(因为背包取了当前物品后容量只会变小),所以从v到w[i]逆向转移,可以保证F[1~j-1]的状态不被破坏。

具体解释如下图:

(5)常数的优化:

有一个很简单的优化,如下面的代码:

for i=1 to n
for j=v to max(v-(w[i]+w[i+1]+……+w[n]),w[i])
F[j]=max(F[j],F[j-w[i]]+c[i])

这也是显然的:如果“我从第i层开始找,以后每一层都取物品,也装不满”,即从最终状态逆向推根本推不到这个状态,那我还计算它干嘛呢。

(6)问题的变形:

有时候背包要求“恰好装满”,这时初始状态就要更改:

F[0]=0, F[1~v]=-inf

原因很简单:只有什么东西不装时,容量为0的背包是”装满“的,其他都不是,因此值为-inf避免选择这些状态。这个技巧对其他背包亦可。

(7)代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>

#define rep(i,x,y) for(int i=x;i<=y;i++)
#define dep(i,x,y) for (int i=x;i>=y;i--)
#define fill(a,x)  memset(a,x,sizeof(a))

using namespace std;

const int maxn=2500+10;

int main()
{ 
  int n,v,num[maxn],f[maxn],c[maxn],w[maxn];
  scanf("%d%d",&v,&n);

  fill(f,0);
  fill(num,0);

  rep(i,1,n) scanf("%d%d",&w[i],&c[i]);
  dep(i,n,1) num[i]=num[i+1]+w[i];

  rep(i,1,n)
   dep(j,v,max(v-num[i],w[i]))
     f[j]=max(f[j],f[j-w[i]]+c[i]);

  printf("%d\n",f[v]);
  return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值