背包问题分类:
- 1、确定dp数组以及下标的含义
- 对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
- 2、确定递推公式,可以有两个方向推出来dp[i][j]
- 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
- 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值,此时 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
- 3、dp数组如何初始化
- 首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。此外,状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
- 4、确定遍历顺序
- 先遍历物品还是先遍历背包重量呢?其实都可以!! 但是先遍历物品更好理解。
- 5、举例推导dp数组
#include <iostream>
#include <vector>
using namespace std;
int main(){
int M, N; cin>>M>>N;
vector<int> weight(M, 0);
vector<int> value(M, 0);
for(int i = 0; i < M; i++) cin>>weight[i];
for(int i = 0; i < M; i++) cin>>value[i];
// dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值
vector<vector<int>> dp(M, vector<int>(N + 1, 0));
// 初始化, 因为需要用到dp[i - 1]的值
for(int j = weight[0]; j < N + 1; j++)
dp[0][j] = value[0];
for(int i = 1; i < M; i++){
for(int j = 0; j < N + 1; j++){
if(j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
cout << dp[M - 1][N]<<endl;
return 0;
}
一维dp和二维dp的写法中,遍历背包的顺序是不一样的!二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!为什么二维dp数组遍历的时候不用倒序,因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!
倒序遍历本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
#include <iostream>
#include <vector>
using namespace std;
int main(){
int M, N; cin >> M >> N;
vector<int> weight(M, 0);
vector<int> value(M, 0); //注意这里不是vector<int> value(N, 0); !!!!!!
for(int i = 0; i < M; i++) cin >> weight[i];
for(int i = 0; i < M; i++) cin >> value[i];
vector<int> dp(N + 1, 0);
for(int i = 0; i < M; i++){
for(int j = N; j >= weight[i]; j--){ //注意这里是j--
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[N] <<endl;
return 0;
}
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(int num : nums) sum += num;
if(sum % 2 != 0) return false;
int capacity = sum / 2;
vector<int> dp(capacity + 1, 0);
for(int i = 0; i < nums.size(); i++){
for(int j = capacity; j >= nums[i]; j--){
// 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
// 集合中的元素正好可以凑成总和capacity
if(dp[capacity] == capacity) return true;
else return false;
}
};