动态规划之背包问题总结

背包问题

    • 背包问题描述
    • 01背包
    • 完全背包
    • 背包问题总结

背包问题描述

(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=0n1kivi
受限于(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=0n1kiwi<=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=0n1kiwi<=C,k{0,1,2,...,+}
(4)方法
方法一:记忆化搜索
方法二:动态规划

01背包

简介: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=weights[i]} \end{array} \right. dp[i][j]={dp[i1][j]max(dp[i1][j],dp[i1][jweights[i]]+profits[i])j<weights[i]j>=weights[i]
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[jw[i]k]+kv[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背包问题 』记忆化搜索、动态规划 + 空间优化

你可能感兴趣的:(【leetcode】,动态规划,leetcode,01背包)