背包问题的整理

dp[i][j]:装前i个物品,背包的可装空间为j的情况下,问题的解(1)能装的最大价值(不一定要装满,只要价值最大)
(2)恰好将背包装满的最大价值
(3)恰好将背包装满有多少种装法

1. 01背包

dp[i][j]表示装前i中物品,背包容积为j的时候的问题答案(最大价值)

状态转移方程:

dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]])
//不装入第i种物品,装入第i种物品的最大值(装入的条件是不超过容积)

所以完整的代码如下:

for(int i=1;i<=n;i++)
for(int j=1;j<=W;j++)
{
	if(j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]);
	else dp[i][j]=dp[i-1][j];
}

j<w[i]时候dp[i][j]的值一定小于等于j>w[i]的情况,因此只需要保留j>w[i]的情况,让第二个for循环的范围是[w[i],W]即可。由于只用到i和i-1相邻状态,可以把i化简去掉。只需要满足,当计算dp[j]的时候,等式右边的dp[j]和dp[j-w[i]]对应的都是i-1时候的取值即可。那么只需要让第二个for循环倒着遍历即可,这样计算到dp[j]的时候,比j小的j-w[i]还没有计算到,还是i-1对应的值

dp[0]=0;//base case
dp[1-n]=0;//对于不超过背包容积的情况,如果要求恰好装满:dp[1-n]=-inf;
for(int i=1;i<=n;i++)
for(int j=W;j>=w[i];j--)
{
	dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
总结0-1和完全背包
j的遍历顺序

0-1背包j的遍历是从大到小,为了保证计算dp[j]的时候等式右边对应的是i-1时候的值,因为j-w[i]比j小,还没更新到,所以还是i-1时候的j-w[i]。

完全背包对j的遍历是从小到大

base case

i的base case就是选择前0个物品(啥也不选)dp[-1][j]=dp[j]
j的base case就是背包容量为0 dp[i][0]

  • 不超过背包容量
    dp[-1][j]=0

  • 恰好装满
    dp[-1][0]=0
    dp[-1][j]=-inf

    dp[i][0]=0

  • 有多少种装法
    dp[-1][0]=1
    dp[-1][j]=0

2. 完全背包

1.恰好将背包装满有多少种

【问题】将硬币凑成指定面值,可以有多少种组合方式? 硬币问题

dp[i][j]表示使用前i种硬币,总面值为j的时候,最多有几种组合方式

总面值=背包容积
每种硬币面值=每种物品重量

状态转移:

不用第i种硬币+用第i种硬币

dp[i][j]=dp[i-1][j]+dp[i][j-coin[i]]

注意:这里对于用第i种硬币为什么是dp[i][j-coin[i]]呢?
其实上数状态转移可以通过公式推导出来,但是也可以直观理解,可以类比爬楼梯问题
用第i种硬币,那么就是至少要用一个,所以求用前 i种硬币凑出j-coin[i]有多少种,每种方法加上这一个第i种硬币,就是使用第i种硬币凑出j的方案。
至于用前i种如何凑出j-coin[i]呢,同理,看状态转移方程,可以用到第i种,也可以不用第i种。
当凑出j-coin[i]不用第i种的时候,对应用前i种凑出j的方案是:用一个第i种硬币
当凑出j-coin[i]用一个第i种的时候,对应用前i种凑出j的方案是:用两个凑出第i种
…以此类推

base case:

因为i=0表示第一个硬币,那么base case表示的相当于i=-1的时候dp[i=-1][j],即什么硬币都不能选的情况下凑出j的方案数。
dp[-1][0]=1;//当总金额为0的时候,什么都不选是唯一能凑成0元的方案
dp[-1][j]=0;//当总金额不为0的时候,什么都不选是凑不出j的,没有方案所以为0

dp[0]=1;//base case,总金额为0的时候
for(int j=1;j<=amount)
	dp[j]=0;//不选择任何硬币的时候。
	
for(int i=0;i<n;i++)
for(int j=coins[i];j<=amount;j++)
{
	//dp[i][j]=dp[i-1][j]+dp[i][j-coins[i]];
	dp[j]=dp[j]+dp[j-coins[i]];
}
2.恰好将背包装满的最大价值
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]
base case

分别对i和j求base case

  • i从0开始表示第一个物品,base case相当于i=-1
    dp[-1][j]:不选择任何物品将j恰好装满,这是不可能完成的,所以用一个-INF表示
  • j的base case是j=0即背包容量为0,那么不选择任何物品恰好将背包装满是可以完成的,dp[i][0]=0
3.不超过背包容积的最大价值
dp[i][j]]=max(dp[i-1][j],dp[i][j-w[i]])
base case

dp[i][j]表示选择前i种物品,不超过容积为j的时候的最大价值
分别求i和j的base case:i=0表示第一个物品,那么base case应该是相当于i=-1的时候,即不选择;j=0表示背包容积为0

dp[-1][j]:不选择任何物品,那么最大价值为0
dp[i][0]:背包容积为0,最大价值0

可以看出,求最大价值的时候,背包转不装满的状态转移方程是一样的,那么二者的区别体现在哪儿呢? 初始化和base case

4.两种求最大价值的完全背包问题的通用代码

背包容积W,每个物品的重量w[i],每个物品的价值v[i],总共有n件物品

for(int i=0;i<=n;i++)
	for(int j=0;j<=W;j++{
		if(j<w[i]) dp[i][j]=dp[i-1][j];
		else dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
	}

化简:
(1)可以发现,找价值最大的情况就是找dp[i-1][j]和dp[i][j-w[i]]这两个数字中较大的,所以直接比较这两个数字的大小即可。为了让后者存在,直接让j循环从w[i]开始,保证j-w[i]一定大于0即可。
(2)同时,只用到了相邻状态,i和i-1之间的值,可以降维,消除i这个维度,只要保证计算dp[j]的时候,等式右边的dp[j]对应的是i-1的值,dp[j-w[i]]对应的是i的值即可。只需要让j从小到大循环,这样当计算到dp[j]的时候,由于j还没计算出来,所以等式右边的dp[j]对应的还是i-1的时候,但是由于j-w[i]<j,所以此时dp[j-w[i]]已经被更新过了,对应的是i的时候的值。
化简后代码如下:

dp[0]=0;//base case
//恰好装满初始化:dp[1-W]=-inf;
//不超过背包容积初始化:dp[1-W]=0;
for(int i=0;i<=n;i++)
	for(int j=w[i];j<=W;j++)
	{
		dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		//dp[j]=dp[j]+dp[j-w[i]];
	}

3.初始化的细节问题

区分初始化和base case

base case的值就是当i/j取某个值(一般是第一个值的前一个)的时候的最优解,是每个动态规划问题都需要给出的。

初始化(目的是为了求最值),对于求最值的问题一般要进行初始化,是对所有的i/j取值,非base case的i/j的初始化值是初始可行解



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值