(1)问题描述
有n个物品,每个物品都有自己的重量和价值,同时给定一个容量为C的背包,记第i件物品的重量为 w i w_i wi, 价值为 v i v_i vi, 求将哪些物品装入背包可使得价值总和最大。
(2)背包分类
01背包:如果限定每件物品最多只能选取 1次(即0 或 1 次),则问题称为 0-1背包问题
完全背包:如果每件物品最多可以选取无限次,则问题称为 完全背包问题
(3)问题定义
假设放入背包中物品i的数目为 k i k_i ki,背包问题在数学表达式上可以记作,
m a x ∑ i = 0 n − 1 k i v i max \sum^{n-1}_{i=0} k_iv_i maxi=0∑n−1kivi
受限于(st.)
01背包
∑ i = 0 n − 1 k i w i < = C , k ∈ { 0 , 1 } \sum^{n-1}_{i=0} k_iw_i <= C,k\in\{0,1\} i=0∑n−1kiwi<=C,k∈{0,1}
完全背包
∑ i = 0 n − 1 k i w i < = C , k ∈ { 0 , 1 , 2 , . . . , + ∞ } \sum^{n-1}_{i=0} k_iw_i <= C,k\in\{0,1,2,...,+\infty\} i=0∑n−1kiwi<=C,k∈{0,1,2,...,+∞}
(4)方法
方法一:记忆化搜索
方法二:动态规划
简介:01背包是一个经典的问题,本文总结了四种01背包的解法
1.递归解法
2.带记忆的递归解法
3.二维dp数组
4.一维dp数组
例子:
有三个物品,每个物品的重量和价值如下
物品 | 重量 | 价值 |
---|---|---|
0 | 1 | 15 |
1 | 3 | 20 |
2 | 4 | 30 |
背包最大重量为4,问背包能背的物品最大价值是多少?
按照动态规划解题5步曲
1.确定dp[i[[j]的含义
dp[i][j]表示从下标为[0…i]的物品里任意取,放进容量为j的背包里,最大的收益是多少
2.dp[i][j]的递推公式
当背包容量j小于物品i重量时,不能放下
dp[i][j] = dp[i-1][j]
当背包容量j大于等于物品i重量时,选择不放或者放
不放, dp[i][j] = dp[i-1][j]
放, dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
因此,dp递推公式可以写成
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] j < w e i g h t s [ i ] m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t s [ i ] ] + p r o f i t s [ i ] ) j > = w e i g h t s [ i ] dp[i][j]=\left\{ \begin{array}{lcl} dp[i-1][j] & & {j
3.初始化dp数组
主要是初始化dp[0][j]和dp[i][0], 因为dp[i][j]可以由dp[i-1][j]或者dp[i-1][j-weight[i]]+value[i]递推得到;
dp[0][j]表示没有物品,因此不管背包容量如何时,dp[0][j]=0;
dp[i][0]表示背包容量,因此不管取哪一个物品时,dp[i][0]=0;
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | ||||
2 | 0 | ||||
3 | 0 | ||||
3 | 0 |
4.确定遍历方式
有两个维度,第一个维度是物品,第二个维度是重量,使用两层for循环
for(int i=1;i<=m;i++){ //0代表没有物品,因此i下标从1开始
for(int j=1;j<=C;j++){ //0代表没有重量,因此j下标从1开始
if(j<weight[i-1]){ //当前背包容量装不下物品i, 因为weight数组是从0开始的,因此使用weight[i-1]表示第i个物品的重量
dp[i][j] = dp[i-1][j];
}else{
//当背包容量j大于weight[i-1]时,dp[i][j]为放和不放物品i中收益的较大者
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i-1]]+value[i-1]);
}
}
}
5.dp数组推导
我们根据2中的递推公式更新dp 表格,dp[i][j] 与 dp[i-1][j] 和 dp[i-1][j-weight[i-1]]+values[i-1] 有关
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 15 | 15 | 15 | 15 |
2 | 0 | 15 | 15 | 20 | 20 |
3 | 0 | 15 | 15 | 20 | 35 |
3 | 0 | 15 | 15 | 20 | 35 |
import java.util.Arrays;
public class ZeroOnePack {
//解法1:递归解法
public int solveZeroOnePack1(int[] weights, int[] profits, int capcity) {
return this.knapsackRecursive(weights, profits, capcity, 0);
}
public int knapsackRecursive(int[] weights, int[] profits, int capcity, int currentItem) {
//base case: 达到物品数量限制 或者剩余背包容量不够
if(currentItem == weights.length || capcity < weights[currentItem]){
return 0;
}
//放
int takeProfit = 0;
if(weights[currentItem] <= capcity){
takeProfit = knapsackRecursive(weights, profits, capcity - weights[currentItem], currentItem + 1)
+ profits[currentItem];
}
//不放
int notTakeProfit = knapsackRecursive(weights, profits, capcity, currentItem + 1);
return Math.max(takeProfit, notTakeProfit);
}
//解法2:带记忆的递归解法
public int solveZeroOnePack2(int[] weights, int[] profits, int capcity) {
int[][] dp = new int[weights.length][capcity+1];
for(int i = 0; i < dp.length; i++){
Arrays.fill(dp[i],-1);
}
return knapsackRecursiveWithMemory(weights, profits, dp, capcity, 0);
}
public int knapsackRecursiveWithMemory(int[] weights, int[] profits, int[][] dp, int capcity, int currentItem) {
//base case: 达到物品数量限制 或者剩余背包容量不够
if(currentItem == weights.length || capcity < weights[currentItem]){
return 0;
}
if(dp[currentItem][capcity] != -1){
return dp[currentItem][capcity];
}
//放
int takeProfit = 0;
if(weights[currentItem] <= capcity){
takeProfit = knapsackRecursiveWithMemory(weights, profits, dp, capcity - weights[currentItem], currentItem + 1)
+ profits[currentItem];
}
//不放
int notTakeProfit = knapsackRecursiveWithMemory(weights, profits, dp, capcity, currentItem + 1);
dp[currentItem][capcity] = Math.max(takeProfit, notTakeProfit);
return dp[currentItem][capcity] ;
}
//解法3:二维dp
public int solveZeroOnePack3(int[] weights, int[] profits, int capcity){
int n = weights.length;
//dp[i][j]表示前i个物品,背包容量为j时的最大收益
int[][] dp = new int[n+1][capcity+1];
//初始化 没有物品或没有容量时都为0
//dp[0][0] = 0; dp[i][0] = 0; dp[0][i] = 0
for(int i=1; i <= n; i++) {
for (int j = 1; j <= capcity; j++){ //逐个尝试背包当前容量
if(j < weights[i-1]){ //背包放不下物品i
dp[i][j] = dp[i-1][j]; //容量为j由前i-1个物品填充
}else{
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weights[i-1]]+profits[i-1]); //比较放和不放物品i的收益大小
}
}
//次层循环结束 得到dp[i][1...capcity]的收益, 表格的第i行被填充
}
return dp[n][capcity];
}
//解法4:一维dp
public int solveZeroOnePack4(int[] weights, int[] profits, int capcity){
int n = weights.length;
//dp[i][j]表示前i个物品,背包容量为j时的最大收益
int[] dp = new int[capcity+1];
//初始化 没有物品或没有容量时都为0
//dp[0] = 0;
for(int i=0; i < n; i++) { //枚举每个物品i
for (int j = capcity; j >= 1; j--){ //逆序枚举当前容量j, 防止一维dp数组 右侧计算好的值被覆盖
if(j >= weights[i]){
dp[j] = Math.max(dp[j], dp[j-weights[i]] + profits[i]); //比较放和不放物品i的收益大小
}
}
// 当前物品收益的计算只与上一个物品有关,可以省略掉 i维度, dp[i][j] -> dp[j],
//dp[j] 是 从右往左填充, capcity 到 1, 因为 dp[j] 的计算 依赖 dp[j-weights[i]], 明显 j > j-weights[i],
}
return dp[capcity];
}
public static void main(String[] args) {
ZeroOnePack zop = new ZeroOnePack();
int[] weights = {2, 3, 1, 4};
int[] profits = {4, 5, 3, 7};
int capcity = 5;
System.out.println("recursive == max profit = " + zop.solveZeroOnePack1(weights, profits, capcity));
System.out.println("recursive with memory == max profit = " + zop.solveZeroOnePack2(weights, profits, capcity));
System.out.println("dp table == max profit = " + zop.solveZeroOnePack3(weights, profits, capcity));
System.out.println("dp table compress == max profit = " + zop.solveZeroOnePack4(weights, profits, capcity));
}
}
在给定背包容量的情况下,一个物品可以放无限多次
d p [ j ] = m a x ( d p [ j ] , d p [ j − w [ i ] ∗ k ] + k ∗ v [ i ] ) ; dp[j] = max(dp[j],dp[j-w[i]*k]+k*v[i]); dp[j]=max(dp[j],dp[j−w[i]∗k]+k∗v[i]);
#include
#include
#define N 6
using namespace std;
int zerosOnepack(int v[],int w[],int n,int C){
int dp[C+1] = {0};
for(int i=1;i<=n;i++){
for(int j=C;j>=w[i];j--){
dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
}
}
return dp[C];
}
int completepack1(int v[],int w[],int n,int C){
int dp[C+1] = {0};
for(int i=1;i<=n;i++){
for(int j=w[i];j<=C;j++){
dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
}
}
return dp[C];
}
int completepack2(int v[],int w[],int n,int C){
int dp[C+1] = {0};
for(int i=1;i<=n;i++){
for(int j=C;j>=w[i];j--){
for(int k=0;k<=C/w[i];k++){//取0件、1件、2件...直到超过限重(k > j/w[i])
dp[j] = max(dp[j],dp[j-w[i]*k]+k*v[i]);
}
}
}
return dp[C];
}
int main(){
int v[N] = {1,3,2,4,5,6};
int w[N] = {1,2,1,3,3,4};
int C = 10;
cout << zerosOnepack(v,w,N,C) << endl;
cout << completepack1(v,w,N,C) << endl;
cout << completepack2(v,w,N,C) << endl;
return 0;
}
类似问题:
恰好装满,最大值,最小值,排列数
未装满的最大值,最小值,排列数
二维状态转移方程:
for i =0 to n: //遍历物品
for j=1 to C: //遍历背包
dp[i][j] = max(dp[i?1][j], dp[i?1][j?w[i]]+v[i]) // j >= w[i]
状态压缩:
for i=0 to n:
for j=C to w[i]:
dp[j] = max(dp[j],dp[j-w[i]]+v[i]
完全背包
for i=0 to n:
for j=w[i] to C:
dp[j] = max(dp[j],dp[j-w[i]]+v[i]
求方案总数
dp[i][j] = sum(dp[i][j], dp[i][j-w[i]]) // j >= w[i]
状态压缩
自己画了下0-1背包的二维数组和一维数组的表格,对一维数组内层循环为什么要使用逆序有了一点小小的突破。
在使用一维数组进行空间优化,内层循环使用倒序时,当前 i 循环内的 dp[j] 会正确使用 前一轮 i 循环的 dp[j],
符合二维状态方程组 dp[i, j] = dp[i - 1, j] + dp[i - 1, j - nums[i - 1]], 当 j > nums[i - 1]时,
亦即当前 dp[j] 的值由它在表格中的同一列的上方和同一列上方左侧的某个值决定;而内层循环使用顺序,
此时的dp[j] 由同一轮 i 循环的 dp[j - nums[i - 1]]决定,亦即d[ j ] 由同一行左侧的某个值决定,
违背了二维状态方程组的语义,也可以说 d[i, j] 由 d[i, j - nums[i - 1]] 推导而来。碰到这类问题,
感觉画图更能清晰地看到问题的本质。
枚举方式
//01背包
for(int i=0; i<n;i++){ //枚举物品
for(int j=W;j>=w[i];j--){ //枚举背包 ,逆向枚举
}
}
//完全背包
for(int i=0; i<n;i++){ //枚举物品
for(int j=0;j<=w[i];j--){ //枚举背包 正向枚举
}
}
什么时候先枚举背包,什么时候先枚举物品?
待续…
参考:
[1] 畅游面试中的动态规划套路-01背包系列
[2] 《背包九讲》
[3] 背包理论基础01背包-2
[4] 『 一文搞懂 0-1背包问题 』记忆化搜索、动态规划 + 空间优化