背包问题总结
转自:https://blog.csdn.net/yandaoqiusheng/article/details/84782655/,背包九讲
1.0-1背包问题
有背包容量为V,N个物品,每个重量为w[i],价值为v[i],每个物品仅有一件,要使得背包价值最大怎么装物品?
定义dp数组含义:dp[i][j]表示考虑前i个物品在背包容量恰为j的情况下达到的最大价值。
状态转移方程:
f[i][j]=max(f[i−1][j],f[i−1][j−w[i]]+v[i])
解释:针对第i个物品有两种选择,一是放进背包,一是不放进背包。max中的第一项就是不放进背包,那么就转换为考虑前i-1件物品放进容量为j的背包;第二项是放进背包,问题转化为前i-1件物品放入容量为j-w[i]的背包,此时的最大价值是再加上当前的物品的价值。
1.1 空间优化
使用滚动数组的思想:
for (int i = 1; i <= n; i++)//遍历物品 for (int j = V; j >= 0; j--)//遍历容量 f[j] = max(f[j], f[j - w[i]] + v[i]);
外层遍历物品,内层要从大到小遍历,因为物品最多只能取一次。(完全背包问题中是从小到大遍历容量,因为可以取无限次。)
只要数量有限制都只能从大到小遍历,其实从原始公式中看的更明显,从大到小只保证使用的是i-1的计算结果。
总结:如果容量从小到大遍历,那么就会重复计算物品,使用的是之前计算的结果,针对完全背包问题;那么针对01背包问题,就需要从后往前,就不会重复计算包了。
力扣例题:416. 分割等和子集
2.完全背包问题
有背包容量为V,N个物品,每个重量为w[i],价值为v[i],每个物品有无数件,要使得背包价值最大怎么装物品?
dp数组含义和上面相同,按照每个物品可取数量可以这么写,
f[i][j]=max(f[i−1][j−k∗w[i]]+k∗v[i])∣0<=k∗w[i]<=j
但是不好解。
2.1 转换为0-1问题
for (int i = 1; i <= n; i++)//遍历物品 for (int j = w[i]; j <= V; j++)//遍历容量 f[j] = max(f[j], f[j - w[i]] + v[i]);
遍历物品,但背包容量是从小到大的,因为物品可以无限次使用。
两层for循环可以替换?
举例:322. 零钱兑换
for(int i=1;i<=amount;i++){//容量在外,每次开始一次都表示之前的容量已确定最终的结果 for(int j=0;j<coins.size();j++){ if(coins[j]>i)continue; dp[i]=min(dp[i],dp[i-coins[j]]+1); } } for(int i=0;i<coins.size();i++){//硬币在外,每次都用一个硬币更新所有的容量一次 for(int j=coins[i];j<=amount;j++){ dp[j]=min(dp[j],dp[j-coins[i]]+1); } }
377. 组合总和 Ⅳ,完全背包问题,求组合数,结果可以重复,所以是对物品的排列,需要先遍历容量,再遍历物品。
总结:
* 1.0-1背包问题,外层遍历物品,内层从大到小遍历容量;(因为只能用一次)
* 完全背包问题,外层便利物品,内层从小到大遍历容量。(能用多次)
70. 爬楼梯,如果本题用背包思想来做,因为是排列问题,不是组合问题,所以需要遍历容量,如果遍历物品的话,那么物品的顺序是定的,就出现方法数变少的错误。
!!!错误的代码!!! class Solution { public: int climbStairs(int n) { //使用完全背包思想来求解 vector<int> stairs={1,2}; vector<int> dp(n+1,0); dp[0]=1; for(int i=0;i<stairs.size();i++){ for(int j=0;j<=n;j++){ if(stairs[i]>j)continue; dp[j]+=dp[j-stairs[i]]; } } return dp[n]; } };
//正确的代码: class Solution3 { public: int climbStairs(int n) { //使用完全背包思想来求解 vector<int> stairs={1,2}; vector<int> dp(n+1,0); dp[0]=1; for(int i=1;i<=n;i++){//排列问题,遍历每个容量,考虑物品顺序 for(int j=0;j<stairs.size();j++){ if(stairs[j]>i)continue; dp[i]+=dp[i-stairs[j]]; } } return dp[n]; } };
2.2 初始化问题
- 要求恰好装满容量V:那么dp[0]=0,其他都初始化为负无穷,表示只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态。
- 不求装满,只求最优:那么所有的数都初始化为0,表示任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0。
3.多重背包问题
每件物品多了个数量限制。
//还没有做过这样得的题目。
4.混合背包问题
有的可以取0-1次,有的可以取无限次,有的能取k次。
那么针对0-1和完全可以分开求解:
p[i]:每个物品的件数,0代表无穷个 for (int i = 1; i <= n; i++)//遍历物品 if (p[i] == 0)//完全背包问题 for (int j = w[i]; j <= V; j++)//从小到大遍历 f[j] = max(f[j], f[j - w[i]] + v[i]); else for (int k = 1; k <= p[i]; k++)//如果p[i]是1,就是0-1背包问题,否则是多重背包问题 for (int j = V; j >= w[i]; j--) f[j] = max(f[j], f[j - w[i]] + v[i]);
//还没有做过这样的题目,希望做的时候能想到解法。
5.二维费用背包问题
现在物品有两种重量,两种价值,当然也有两个背包,也就是有两个限制,那么就再多加一维状态:
定义dp数组含义:dp[i][j][k]表示考虑前i件物品,背包容量分别为j和k时能达到的最大价值。
状态转移:
max(f[i−1][j][k],f[i−1][j−w[i]][k−g[i]]+v[i])//不放、放
5.1 空间优化
那么此时就需要从大到小遍历:
for (int i = 1; i <= n; i++) for (int j = V; j >= w[i]; j--)//两层均需要从大到小遍历 for (int k = T; k >= g[i]; k--) f[j][k] = max(f[j][k], f[j - w[i]][k - g[i]] + v[i]);
时间复杂度就是三层for循环乘积了,空间复杂度降成了二维。
题目:474. 一和零