01背包:经典DP问题( 基本/滚动数组(优化)/恰好装满 )

        所谓01背包问题,是指对于一定数量( i )的物品有一个容量为( j )的背包,每个物品都有自己的容量( k )、价值(value)。在保证物品容量之和不大于背包容量的前提下,如何选取物品得到最大价值?注意每个物品只能取一次,所以说是每个物品不是0个就是1个,称为01背包问题。

        比如说给出最大容量是6,三个物品的容量和价值分别是2,5;3,8;4,9。则取容量是2和4的物品得到的价值最大,为14。这里用到的是经典的动态规划思想( DP )。

        分解问题,把物品一件一件的放入背包内,实时记下当前所有可能容量的最大价值。如上面的例子中,先放容量2价值5的物品,容量在2-6的所有可能放下该物品的背包都存下该物品,用数组记录dp[1][[2]~dp[1][6]的值为5。余下的dp[1][1]保持0不变。

        之后放入第二件物品,容量是3,价值是8。可能放下该物品的背包容量是3-6,此时需要判断在这些背包容量中该不该放该物品!容量是3的背包此时里面是刚才的容量为2的物品,把容量是3的物品放进去,价值会更高( 8>5 ),而容量是5和6的背包可以同时放下两个物品,最大价值就是5+8=13。用数组记录dp[2][3]~dp[2][4]的值为8,dp[2][5]~dp[2][6]的值为13。余下的dp[2][1]~dp[2][2]同dp[1][1]~dp[1][2]。

        放入最后一件物品,容量4,价值9。可能放下该物品的背包容量是4-6,同上面的方法,dp[3][4] = 9;dp[3][5] = 13;dp[3][6] = 14。( dp[2][2]+9 )

        由此可以推出,每次存放物品都需要判断当前背包容量减去当前物品容量(k)的最大价值加上当前物品的价值( value )是否大于原价值。如果大于就刷新最大值。状态转移方程dp[ind][jnd]=Max(dp[ind-1][jnd-k]+value,dp[ind-1][jnd])。这个方程用在可能放下该物品的前提下!如果当前物品容量大于当前背包容量则不计算,存之前此背包容量的价值即可。dp[ind][jnd]=dp[ind-1][jnd]

         存入物品个数( i )和总容量( j ),再得到i个物品的容量和价值后。跑一遍状态转移方程,时间和空间复杂度都是O( i*j )。

        对于最多有1000个物品,背包最大容量是10000的01背包问题。

#include<stdio.h>
#define Max(a,b) (a>b?a:b)
int dp[1005][10005];
int k[1005], value[1005];
int main(int argc, char* argv[])
{
	int i, j;
	scanf("%d %d", &i, &j);
	for (int ind = 1; ind <= i; ++ind)
		scanf("%d %d", &k[ind], &value[ind]);
	for (int ind = 1; ind <= i; ++ind)
	{
		for (int jnd = 1; jnd <= j; ++jnd)
		{
			if (jnd - k[ind] < 0)
				dp[ind][jnd] = dp[ind - 1][jnd];
			else
				dp[ind][jnd] = Max(dp[ind - 1][jnd], dp[ind - 1][jnd - k[ind]] + value[ind]);
		}
	}
	printf("%d\n", dp[i][j]);
	return 0;
}

        dp数组是1005*10005,这就显得有些大了,如何优化空间复杂度呢?那就是神奇的滚动数组!上面用状态转移方程一层一层的存储下了每次放物品的所有容量的最大价值,这就会显得没有必要,因为最后需要的只有最后一行的最大容量的价值。所以说根据上一行得到下行数据后上一行的数据就是没有用处的了。于是用一个一维数组dp[10005]存储当前的各容量价值即可,每次输入一个物品后,根据它的容量和价值和dp数组历史值刷新dp数组就可以啦。

        具体是怎么个滚动法呢,得到一个输入物品容量和价值后,从最大容量背包( j )到可以放下当前物品的最小背包容量( k )依次判断,dp[ind] = Max(dp[ind-k]+value,dp[ind]) 。后面这个状态转移方程和上面的相同,但是上面的所有可能背包容量必须从最大值到最小值。这里可能很难马上理解,因为如果从最小值开始,那么比它大的背包容量就可能取到多次当前物品!比如说在还是空的的容量是10的背包里放容量价值都是1的物品,dp[1] = 1, dp[2] = dp[1] +1 = 2。显然这里出错误了,之后一直到dp[10]一直会递增下去。错误的根本原因是,从dp[2]开始状态转移方程用到的dp[ind-k]并不是历史数据了,而是放入当前物品刷新之后的dp数组。因为ind-k肯定是小于ind的,所以应该从最大值开始滚动。这样每次滚动用到的历史数据都会保证是没放入当前物品的旧数据。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define Max(a,b) (a>b?a:b)
int dp[10005];
int main(int argc, char* argv[])
{
	int i, j;
	scanf("%d %d", &i, &j);
	while (i--)
	{
		int k, value;
		scanf("%d %d", &k, &value);
		for (int ind = j; ind >= k; ind--)
			dp[ind] = Max(dp[ind - k] + value, dp[ind]);
	}
	printf("%d\n", dp[j]);
	return 0;
}

    使用滚动数组动态规划,时间空间复杂度都多少有所优化,空间优化最为显著,缩小到O( j )。

    **那么如何继续优化滚动数组呢?每一个物品的"滚动"都是背包最大容量到能装下物品的最小容量( k ),这会不会有冗余部分呢?考虑一下,在后面物品用不到之前滚动的历史数据时,之前滚动的时候就没必要滚动那么多了吧。用到的历史数据就是dp[ind-k]项,取极限情况的时候,后面所有物品的容量加起来一同放进背包时,背包剩余的容量( x )大于最小容量( k )。这种情况下当前物品的滚动就不必到 k 了,而应该取x。所以说优化后的滚动数组应用Max(k,x)代替k,在背包总容量较大时,优化会很显著,缺点是不能在输入的同时滚动了,因为需要待全部物品输入完毕后才能得到相应的x值。

    简单的用数据说明一下,如果背包总容量是10,当前物品的容量是5,那么没有优化前的滚动数组应该从dp[10]~dp[5]。而后面的物品分别是容量为1和2,加起来总容量不过是3,当把容量是3的物品放进背包时,就需要判断到底值不值,用到了dp[10-3]=dp[7]的数据,那么dp[7]将是后面物品可能用的到的最小背包容量,也就是说后面可能用的到的只有dp[7]~dp[10]。所以经过优化当前容量为5的物品滚动数组的变化是dp[10]~dp[7]。

    透彻明白DP是怎么解决背包问题的之后,理解这个优化方法应该比较轻松。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define Max(a,b) (a>b?a:b)
int dp[10005];
int k[1005], value[1005];
int main(int argc, char* argv[])
{
	int i, j;
	scanf("%d %d", &i, &j);
	int s = 0;
	for (int ind = 1; ind <= i; ind++)
	{
		scanf("%d %d", &k[ind], &value[ind]);
		s = s + k[ind];
	}
	for (int ins = 1; ins <= i; ins++)
	{
		s -= k[ins];
		for (int ind = j; ind >= Max(j - s, k[ins]); ind--)
			dp[ind] = Max(dp[ind - k[ins]] + value[ins], dp[ind]);
	}
	printf("%d\n", dp[j]);
	return 0;
}

    s 是当前物品之后所有物品的容量之和,j-s 就是前面提到的 x 。

    **进一步提出问题,要求在选取的物品容量之和等于背包总容量的前提下,得到最大价值。这就需要筛选掉背包内有空余容量的所有情况了,在现有解法的基础上只需要将dp[1]~dp[j]的初始值设定成无限小就可以了。在放第一个物品进背包的时候因为只有dp[0]的初始值是 0 ,所以只有恰好能放下这个物品的背包才会存下这个物品,之后每次放新物品都只能是之前放过物品的容量和恰好能放下当前物品容量的背包才会有效判断是否该刷新最大价值。

    

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define Max(a,b) (a>b?a:b)
int dp[10005];
int k[1005], value[1005];
int main(int argc, char* argv[])
{
	int i, j;
	scanf("%d %d", &i, &j);
	memset(dp + 1, -1, j * sizeof(int));
	int s = 0;
	for (int ind = 1; ind <= i; ind++)
	{
		scanf("%d %d", &k[ind], &value[ind]);
		s = s + k[ind];
	}
	for (int ins = 1; ins <= i; ins++)
	{
		s -= k[ins];
		for (int ind = j; ind >= Max(j - s, k[ins]); ind--)
			if (dp[ind - k[ins]] != -1)
				dp[ind] = Max(dp[ind - k[ins]] + value[ins], dp[ind]);
	}
	printf("%d\n", dp[j]);
	return 0;
}

    用 -1 表示无穷小,当用到的历史数据 dp[ind-k] 是 -1 时直接跳过。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值