1.什么是背包问题
背包模型是一个经典的 dp (dynamic programming,动态规划)的模型。 背包问题的描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
2. 01背包
01背包又是背包问题里面最经典,也是最基础的问题,其他的背包问题都可以由01背包问题转化而来。
01 背包的问题描述为:给定一组物品,每个物品都有各自的体积 和 价值。对于每一个物品,我们只有选和不选 两种方案。要如何选择才能在不超过背包容量的情况下,使总的价值最大。
2.1 问题
有 N N N 件物品和一个容量是 V V V 的背包。每件物品只能使用一次。
第 i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数, N N N, V V V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N N N 行,每行两个整数 v i v_i vi, w i w_i wi,用空格隔开,分别表示第 i i i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
- 0 < N , V ≤ 1000 0<N,V≤1000 0<N,V≤1000
- 0 < v i , w i ≤ 1000 0< v_i,w_i ≤1000 0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
2.2 问题分析
- 朴素版代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N][N],v[N],w[N];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++) scanf("%d%d",&v[i],&w[i]);
for(int i = 1;i <= n;i++){
for(int j = 0;j <= m;j++){
//不选第 i 个物品
f[i][j] = f[i-1][j];
//选第 i 个物品,前提是背包容量 j 要 >= 第 i 个物品的体积 v[i]
if(j >= v[i])
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
/*for(int j = 0;j <= m;j++) cout<<f[i][j]<<" ";
puts("");*/
}
cout<<f[n][m]<<'\n';
return 0;
}
输出结果为:
0 2 2 2 2 2
0 2 4 6 6 6
0 2 4 6 6 8
0 2 4 6 6 8
8
- 优化版
我们实际上可以优化掉第一维,也就是只使用一维数组来进行求解。定义f(j) 为从前 i 个物品中选,体积不超过j的最大值。
注意:如果我们直接将第一维去掉,会出现问题
有问题的代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N],v[N],w[N];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++) scanf("%d%d",&v[i],&w[i]);
for(int i = 1;i <= n;i++){
for(int j = 0;j <= m;j++){
//不选第 i 个物品
f[j] = f[j];//这一句实际可以去掉
//选第 i 个物品,前提是背包容量 j 要 >= 第 i 个物品的体积 v[i]
if(j >= v[i])
f[j] = max(f[j],f[j-v[i]]+w[i]);
}
for(int j = 0;j <= m;j++) cout<<f[j]<<" ";
puts("");
}
cout<<f[m]<<'\n';
return 0;
}
错误的结果 :
0 2 4 6 8 10
0 2 4 6 8 10
0 2 4 6 8 10
0 2 4 6 8 10
10
问题分析:
优化版的代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N],v[N],w[N];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++) scanf("%d%d",&v[i],&w[i]);
for(int i = 0;i <= n;i++){
for(int j = m;j >= v[i];j--){
f[j] = max(f[j],f[j-v[i]]+w[i]);
}
for(int j = 0;j <= m;j++) cout<<f[j]<<" ";
puts("");
}
cout<<f[m]<<'\n';
return 0;
}
3. 例题
3.1 例题1
给你一个 只包含正整数 的 非空 数组 n u m s nums nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
- 1 ≤ n u m s . l e n g t h ≤ 200 1 \leq nums.length \leq 200 1≤nums.length≤200
- 1 ≤ n u m s [ i ] ≤ 100 1 \leq nums[i] \leq 100 1≤nums[i]≤100
题目分析:
首先求出数组所有元素的和
s
u
m
sum
sum。如果
s
u
m
sum
sum 是奇数,那么无论如何数组
n
u
m
s
nums
nums 也不能分为两个相同的子集,直接返回 false
即可。
设 m = s u m / 2 m = sum / 2 m=sum/2, m m m 就是背包的容量,数组元素既看作 价值 ,又看作 体积。用 01背包dp,看最后能否装满这个容量为 m m m 的背包。如果可以就证明 n u m s nums nums 可以被分割为两个大小相同的子集;否则就不行。
时间复杂度: O ( m × n ) O(m \times n) O(m×n)
C++代码:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
int n = nums.size();
for(auto &num:nums) sum += num;
if(sum % 2) return false;
int m = sum/2;
bool f[m + 1];
memset(f,0,sizeof f);
f[0] = 1;
for(int i = 0;i < n;i++){
for(int j = m;j >= nums[i];j--){
f[j] |= f[j - nums[i]];
}
}
return f[m];
}
};
JAVA代码:
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
int n = nums.length;
for(int x:nums) sum += x;
if(sum % 2 == 1) return false;
int m = sum / 2;
boolean[] f = new boolean[m+1];
f[0] = true;
for(int i = 0;i < n;i++){
for(int j = m;j >= nums[i];j--){
f[j] |= f[j-nums[i]];
}
}
return f[m];
}
}
3.2 例题2
有一堆石头,用整数数组 s t o n e s stones stones 表示。其中 s t o n e s [ i ] stones[i] stones[i] 表示第 i i i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x x x 和 y y y,且 x ≤ y x \leq y x≤y。那么粉碎的可能结果如下:
- 如果 x = y x = y x=y,那么两块石头都会被完全粉碎;
- 如果 x ≠ y x \neq y x=y,那么重量为 x x x 的石头将会完全粉碎,而重量为 y y y 的石头新重量为 y − x y-x y−x。
最后,最多只会剩下 一块石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0 0 0。
示例 1:
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
示例 2:
输入:stones = [31,26,33,21,40]
输出:5
提示:
- 1 ≤ s t o n e s . l e n g t h ≤ 30 1 \leq stones.length \leq 30 1≤stones.length≤30
- 1 ≤ s t o n e s [ i ] ≤ 100 1 \leq stones[i] \leq 100 1≤stones[i]≤100
题目分析:
时间复杂度:
O
(
m
×
n
)
O(m \times n)
O(m×n)
C++代码:
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for(auto &num:stones) sum += num;
int m = sum / 2;
int f[m+1];
memset(f,0,sizeof f);
for(int i = 0;i < stones.size();i++){
for(int j = m;j >= stones[i];j--){
f[j] = max(f[j],f[j-stones[i]]+stones[i]);
}
}
return (sum - f[m]) - f[m];
}
};
Java代码:
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
int n = stones.length;
for(int x:stones) sum += x;
int m = sum / 2;
int[] f = new int[m+1];
for(int i = 0;i < n;i++){
for(int j = m;j >= stones[i];j--){
f[j] = Math.max(f[j],f[j-stones[i]]+stones[i]);
}
}
return (sum - f[m]) - f[m];
}
}