文章目录
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]=-infdp[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的初始化值是初始可行解