背包总结
背包问题通常是多种物品有多个属性,且已知条件为某属性被受限,求另一属性的最大/最小/等于/存在不存在。以0-1背包为例解释:n个物品具有的属性为重量和价值,其中总重量C将重量的属性限制住,求最大价值,即求另一属性的特征。
针对背包问题:
1、先判断属于0-1背包还是完全背包。
2、看是求最大值/最小值/等值/是否存在/排列/组合(排列/组合问题通常出现在完全背包中)。
确定了背包类型及要求的问题后,即可下手做题。
1、0-1背包
0-1背包是指物品只能使用一次,故通常其状态方程为:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
w
i
]
+
v
i
)
dp[i][j] = max(dp[i-1][j],dp[i-1][j-wi]+vi)
dp[i][j]=max(dp[i−1][j],dp[i−1][j−wi]+vi)
式中:dp[i][j]
:将前i个物品放入容量为j的背包中可得的最大价值。dp[i-1][j]
是指不取第i个物品所的最大价值
dp[i-1][j-wi]+vi
则为取i个物品时所得的最大价值。
同样,若求最小值则为**dp[i][j] = min(dp[i-1][j],dp[i-1][j-wi]+vi)
。**
若求等值个数则为
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
+
d
p
[
i
−
1
]
[
j
−
w
i
]
)
dp[i][j] =dp[i-1][j]+dp[i-1][j-wi])
dp[i][j]=dp[i−1][j]+dp[i−1][j−wi])
式中:dp[i][j]
:前i个元素可组成和为j的个数。dp[i-1][j]
是指不取第i个物时,前i-1个物品可组成和为j的个数
dp[i-1][j-wi]+vi
则为取i个物品时可组成和为j的个数。
或求是否存在的题,状态转移为:
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
∣
∣
d
p
[
i
−
1
]
[
j
−
w
i
]
)
dp[i][j] = dp[i-1][j] || dp[i-1][j-wi])
dp[i][j]=dp[i−1][j]∣∣dp[i−1][j−wi])
式中:dp[i][j]
:前i个元素是否可组成和为j的个数。dp[i-1][j]
是指不取第i个物时,前i-1个物品是否可组成和为j的个数
dp[i-1][j-wi]+vi
则为取i个物品时是否可组成和为j的个数。
在了解完0-1基本概念及相关问题后,可进一步编程实现。建议先看下文中0-1背包问题,详细介绍了背包问题的思路及优化方法。。。现以一道0-1背包题解释优化后的代码(套路,理解了即可解决许多问题)。
**输入:**第一行输入两个整数N和X(N代表物品个数、X代表背包容量)
第二行输入第1个物品的价格A1和所占容量B1
第N+1行代表第第1个物品的价格A2和所占容量B2
**输出:**在背包容量X内所的最大价格
分析:首先,该题符合0-1背包。其次要求计算最大价格。那么套用之前的状态转移方程可得代码:
#include "pch.h"
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
int N, X;
cin >> N >> X;
vector<int> A(N, 0);
vector<int> B(N, 0);
for (int i = 0; i < N; i++)
{
cin >> A[i] >> B[i];
}
// 1、优化后计算只需一维vector即可
vector<int> dp(X + 1, 0);
for (int i = 0; i < N; i++)
{
// 2、根据状态方程知道,每次获得值时,其实只需要上一层的值及该层之前的值,故只要逆序获得值,即不会覆盖上一层的值
for (int j = X; j >= B[i]; j--)
{
dp[j] = max(dp[j], dp[j - B[i]]+A[i]); // 3、dp[j]为不取第i个值时,所得最大价值。另一个为取该值时(j-A[i])容量所得最大价值
}
}
cout << dp[X];
}
另:leetcode416为判断是否存在的问题,可自行思考,现附上代码:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int len = nums.size();
// 求总数
int sum = 0;
for(int i=0;i<len;i++)
{
sum += nums[i];
}
// 若为奇数,直接返回0
if(sum % 2) return 0;
int target = sum >> 1;
vector<bool> dp(target+1,0);
dp[0] = true; // 初始值可先忽略,然后在分析的时候就可知应为多少
for(int i=0;i<len;i++)
{
for(int j=target;j>=nums[i];j--) // 注意范围
{
dp[j] = dp[j] || dp[j-nums[i]];
}
}
return dp[target];
}
};
2、完全背包
完全背包与0-1的差别在于,完全背包中每个物品的取值无限制,故对于dp[i][j]
,当取i时,则dp[i][j]
为dp[i][j-wi]+vi
(由于i有无限个,故取i时,则仍位于i处,而不是i-1处。这处即为二者的区别)。。由于状态转义方程有些许差别,其在代码编写过程中,也不一样。。以leetcode322为例:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int len = coins.size();
// vector<long> dp(amount+1,INT_MAX);
vector<long> dp(amount+1,amount+1); // 1、若选用INT_MAX过于浪费空间(因为硬币不可能为0,至少为1,故最多也不超过amount+1)
dp[0] = 0; // 2、初始值在分析问题时确定
for(int i=0;i<len;i++)
{
for(int j = coins[i];j<=amount;j++) // 3、注:无限背包需要本层的之前元素,故直接从头覆盖即可(不懂得见状态方程)
{
dp[j] = min(dp[j],dp[j-coins[i]]+1); // 每次取i都得加一个硬币,这也是为啥dp[0] = 0
}
}
return dp[amount] >= amount+1 ? -1:dp[amount];
}
};
另外,还需注意的是,完全背包在求得到目标的组合个数时,通常会有排序和不排序两种方法。具体编程实现也有区别,见leetcode518与377.
class Solution {
public:
int change(int amount, vector<int>& coins) {
int len = coins.size();
vector<int> dp(+1,0);
dp[0] = 1;
for(int i=0;i<len;i++) // 与排列顺序无关,故按之前套路来即可
{
for(int j = coins[i];j<=amount;j++)
{
dp[j] = dp[j]+ dp[j-coins[i]];
}
}
return dp[amount];
}
};
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
int len = nums.size();
vector<long> dp(target+1,0);
dp[0] = 1; // 1、状态转移方程求得是组合,与硬币数不一样,故初始化应为1
for(int i=1;i<= target;i++) // 求排列的问题,则应该按此种模板进行
{
for(int j = 0;j<len;j++)
{
if(nums[j] <= i)
dp[i] = (dp[i] + dp[i-nums[j]])%INT_MAX; // dp[i-nums[j]] 意味着,目标值为i-nums[i]对应点组合数
}
}
return dp[target];
}
};