背包问题分类
图片来自leetcode用户代码随想录
的题解中:
0-1背包问题
问题描述:有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每件物品都只有1件。
样例:
5 8 // n==5, V==8
3 5 1 2 2 // w[i]
4 5 2 1 3 // c[i]
答案:10
使用暴力法时间复杂度为O(2n),可以使用二维DP解决该问题,时间复杂度为O(nV)。
其中dp[i][j]
表示:在前i个物品中挑选若干,使容量为j的背包,能装下的最大价值。
那么状态转移方程即为dp[i][j]=max(dp[i-1][j],value[i]+dp[i-1][j-weight[i]]);
其含义是:在背包容量为j
的情况下,装第i
个物品,和不装第i
个物品,选择使背包中物品价值更大的方案。
其中,dp[i-1][j]
表示在前i-1
个物品中挑选,使得容量为j
的背包能够达到的最大价值。
对于value[i]+dp[i-1][j-weight[i]]
,value[i]
表示选中了第i
个物品;dp[i-1][j-weight[i]]
表示去掉选中的第i
个物品占有的重量,剩下的背包部分能装下的最大价值。
以下是上述样例的二维dp数组,注意dp[i][j]
表示的是什么。
动态规划的思路是:物品一个一个尝试,容量一点一点尝试,每个物品分类讨论的标准是:选与不选
C++代码如下:
#include<vector>
#include<algorithm>
#include<iostream>
using namespace std;
int findLargestValue(vector<int> weight, vector<int> value, int N, int V) {
vector<vector<int> > dp(N, vector<int>(V + 1, 0));
//初始化第一行
for (int weighti = 1; weighti <= V; weighti++) { //这里的i是背包容量
if (weighti >= weight[0]) {
dp[0][weighti] = value[0];
}
}
//状态转移方程:dp[i][j]=max(dp[i-1][j],value[i]+dp[i-1][j-weight[i]]); 即选中和不选中两种方法
for (int i = 1; i < N; i++)
for (int j = 1; j <= V; j++) {
if (j - weight[i] < 0)
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], value[i] + dp[i - 1][j - weight[i]]);
}
return dp[N - 1][V];
}
int main() {
int arr1[] = {3, 5, 1, 2, 2};
int arr2[] = {4, 5, 2, 1, 3};
vector<int> weight;
for (int i = 0; i < 5; i++)
weight.push_back(arr1[i]);
vector<int> value;
for (int i = 0; i < 5; i++)
value.push_back(arr2[i]);
cout << findLargestValue(weight, value, 5, 8);
return 0;
}
很多类似问题可以转化为0-1背包问题,如leetcode416题
将分割为两个和相等子集的问题,转换为数字中挑数以组成固定和的问题,即01背包问题。
#include <vector>
#include <numeric>
using namespace std;
class Solution {
public:
bool canPartition(vector<int> &nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2 == 1)
return false;
int target = sum / 2;
//dp[i][j]表示,在nums,0~i中挑选数字,能否组合成j
vector<vector<bool> > dp(nums.size(), vector<bool>(target + 1, false));
//对第一行进行初始化
for (int i = 1; i <= target; i++) {
if (i == nums[0])
dp[0][i] = true;
else
dp[0][i] = false;
}
//对第一列进行初始化
for (int i = 0; i < nums.size(); i++) {
dp[i][0] = true;
}
//开始进行动态规划
for (int i = 1; i < nums.size(); i++)
for (int j = 1; j <= target; j++) {
if (dp[i - 1][j])
dp[i][j] = true;
else {
if (j - nums[i] >= 0 && dp[i - 1][j - nums[i]])
dp[i][j] = true;
}
}
return dp[nums.size()-1][target];
}
};
进一步对空间进行压缩
//多维费用问题
class Solution {
public:
pair<int,int> count(const string&s){
int count0=s.length(),count1=0;
for(const char&c:s){
if(c=='1'){
count1++;
count0--;
}
}
return make_pair(count0,count1);
}
int findMaxForm(vector<string>& strs, int m, int n) {
//dp[i][j]表示0有i个,1有j个地情况下,最多能容纳几个字符串
vector<vector<int> >dp(m+1,vector<int>(n+1,0));
for(const string&str:strs){
auto [count0,count1]=count(str);
//倒序
for(int i=m;i>=count0;--i)
for(int j=n;j>=count1;--j){
dp[i][j]=max(dp[i][j],1+dp[i-count0][j-count1]);
}
}
return dp[m][n];
}
};
如果想使用压缩dp数组来节省空间,那么原本的二维dp只需要一维,原本的三维dp只需要二维,但是注意,在下一轮利用上一轮的dp数据时,因为是原地修改,需要倒序遍历修改,否则会造成上一轮的临时数据被修改(顺序遍历会导致多次取用本轮的同一个物品,不符合01背包题意)
- 普通01背包和多维费用01背包问题,在进行动态规划时,解题形式相似,如下
普通01背包
普通01背包压缩空间
多维费用01背包
多维费用01背包压缩空间后