动态规划-背包问题

本文详细介绍了背包问题的三种类型:0-1背包、完全背包和多重背包,并提供了Java实现的动态规划算法。0-1背包不允许有重复物品,完全背包允许无限数量的同种物品,多重背包则限制了每种物品的数量。通过状态转移方程和滚动数组优化,实现了空间效率的提升。此外,还探讨了如何通过回溯找到物品选择路径。文章以LeetCode的零钱兑换问题为例,展示了背包问题的实际应用。
摘要由CSDN通过智能技术生成


前言

最近在学习各种背包问题 动态规划知识 记个博客录一下


一、0-1背包

问题描述:
一共有N件物品,第i(i从1开始)件物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少?

状态 选择:
状态有两个,就是「背包的容量」和「可选择的物品」
选择就是「装进背包」或者「不装进背包」

定义状态dp:
dp[i][j]表示将前i件物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W
base case
就是dp[0][…] = dp[…][0] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。

  1. 不装入第i件物品,即dp[i−1][j];
  2. 装入第i件物品(前提是能装下),即dp[i−1][j−w[i]] + v[i]。

状态转移方程为:
// j >= w[i]
dp[i][j] = max(dp[i−1][j], dp[i−1][j−w[i]]+v[i])

代码:

// java版本
int knapsack(int [] wt, int [] val, int W) {

	int N = val.length;
	int [][] dp = new int[N+1][W+1];
	
	
    //base case 初始化 限重为0的背包最大价值
    for(int i = 0;i <= N;i++){
    	dp[i][0] = 0;
    }
    for(int i = 0;i <= W;i++){
    	dp[0][i] = 0;
    }
    
    for (int i = 1; i <= N; i++) {
        for (int j = 1; j <= W; j++) {
            if (j - wt[i-1] < 0) {
                // 当前背包容量装不下,只能选择不装入背包
                dp[i][j] = dp[i - 1][j];
            } else {
                // 装入或者不装入背包,择优
                dp[i][j] = max(dp[i - 1][j - wt[i-1]] + val[i-1], 
                               dp[i - 1][j]);
            }
        }
    }
    return dp[N][W];
}

压缩状态 一维数组优化版
0-1背包 第二循环逆序
由上述状态转移方程可知,dp[i][j]的值只与dp[i-1][0,…,j-1]有关,所以我们可以采用动态规划常用的方法(滚动数组)对空间进行优化(即去掉dp的第一维)。需要注意的是,为了防止上一层循环的dp[0,…,j-1]被覆盖,循环的时候 j 只能逆向枚举(空间优化前没有这个限制)

定义状态dp:
dp[i]表示将N件物品装进限重为i的背包可以获得的最大价值, 0<=i<=W

代码为:

// java版本
int knapsack(int [] wt, int [] val, int W) {

	int N = val.length;
	int []dp = new int[W+1];
	
    //base case 初始化 
    for(int i = 0; i <= W;i++){
		dp[i] = 0;
	}
    
    for (int i = 0; i < N; i++) {
        for (int j = W; j >= 0; j--) {
            if (j - wt[i] >= 0) {
                // 不装或装入背包,择优
                dp[j] = max(dp[j], dp[j - wt[i]] + val[i]);
            }
        }
    }
    return dp[W];
}

背包问题 求路径问题

必须用二维数组记录

参考链接

// java版本
import java.util.*;
public class Main {
    public static int knapsack(int [] wt, int [] val,int W) {

        int N = val.length;
        int [][] dp = new int[N+1][W+1];


        //base case 初始化 限重为0的背包最大价值
        for(int i = 0;i <= N;i++){
            dp[i][0] = 0;
        }
        for(int i = 0;i <= W;i++){
            dp[0][i] = 0;
        }

        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= W; j++) {
                if (j - wt[i-1] < 0) {
                    // 当前背包容量装不下,只能选择不装入背包
                    dp[i][j] = dp[i - 1][j];
                } else {
                    // 装入或者不装入背包,择优
                    dp[i][j] = Math.max(dp[i - 1][j - wt[i-1]] + val[i-1],
                            dp[i - 1][j]);
                }
            }
        }

        //物品是否被选择
        int[] x = new int[N+1];

        //从状态矩阵右下角的最优值开始往前回溯
        for(int i=N;i>1;i--) {
            //如果当前状态的解等于上一个状态的解,表示当前物品没有放进背包
            if(dp[i][W] == dp[i-1][W]){
                x[i]=0;
            }
            //如果当前状态的解不等于上一个状态的解,说明当前物品被放进背包
            else {
                x[i] = 1;
                W -= wt[i-1];//当前状态的最优解是通过m[i-1][c-wt[i-1]]+v[i-1]得到的,
                //因此下一步从dp[i-1][W-wt[i-1]]开始继续回溯,
                //按照同样的方法判断第i-1个物品有没有被装进背包
            }
        }
        //上面的for循环可以判断第2,3,4……个物品是否被装进背包
        //因此还需要单独对第一个物品进行判断
        //如果经过上面的for循环之后背包剩余容量c能够装下第一个物品
        //也就是m[1][c]>0,说明第一个物品肯定被装进了背包
        x[1] = (dp[1][W]>0) ? 1 : 0;

        for (int i = 1; i <= N; i++) {
            System.out.print(x[i]);
        }

        return 0;
    }


    public static void main(String[] args) {

        int[] w = new int[]{0, 4, 6, 2, 2, 5, 1};
        int[] v = new int[]{0, 8, 10, 6, 3, 7, 2};

        int N = v.length;
        int W = 12;
        knapsack(w,v,W);

    }
}

二、完全背包

问题描述:
完全背包(unbounded knapsack problem)与01背包不同就是每种物品可以有无限多个:
一共有N种物品,每种物品有无限多个,第i(i从1开始)种物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少?

状态 选择:
和0-1背包一样
状态有两个,就是「背包的容量」和「可选择的物品」
选择就是「装进背包」或者「不装进背包」

定义状态dp:
和0-1背包一样
dp[i][j]表示将前i件物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W
base case
就是dp[0][…] = dp[…][0] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。

  1. 不装入第i件物品,即dp[i−1][j];
  2. 装入第i件物品(前提是能装下),即dp[i][j−w[i]] + v[i]。和0-1背包不同!!!

状态转移方程为: 和0-1背包不同!!!
// j >= w[i]
dp[i][j] = max(dp[i−1][j], dp[i][j−w[i]]+v[i])

代码

//二维数组的解法
//代码和0-1背包类似
//只是状态转移方程不同

压缩状态 一维数组优化版
完全背包 第二循环正序
也可进行空间优化,优化后不同点在于这里的 j 只能正向枚举而01背包只能逆向枚举!!!,因为这里的max第二项是dp[i]而01背包是dp[i-1],即这里就是需要覆盖而01背包需要避免覆盖。

代码和0-1背包最大的不同就是正向枚举!!!

int knapsack(int [] wt, int [] val, int W) {

	int N = val.length;
	int []dp = new int[W+1];
	
    //base case 初始化 
    for(int i = 0; i <= W;i++){
		dp[i] = 0;
	}
    
    for (int i = 0; i < N; i++) {
        for (int j = 0; j <= W; j++) {
            if (j - wt[i] >= 0) {
                // 不装或装入背包,择优
                dp[j] = max(dp[j], dp[j - wt[i]] + val[i]);
            }
        }
    }
    return dp[W];
}

例题:
leetcode
322. 零钱兑换
题目:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。你可以认为每种硬币的数量是无限的。
518. 零钱兑换 II
题目:给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

三、多重背包

多重背包(bounded knapsack problem)与前面不同就是每种物品是有限个:一共有N种物品,第i(i从1开始)种物品的数量为n[i],重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少?

我们从装入第 i 种物品多少件出发:装入第i种物品0件、1件、…n[i]件(还要满足不超过限重 j/w[i])。
k为装入第i种物品的件数, k <= min(n[i], j/w[i])

所以状态方程为:
dp[i][j] = max{(dp[i-1][j − kw[i]] + kv[i]) for every k}

同理也可以进行空间优化,而且 j 也必须逆向枚举,优化后伪代码为

// 完全背包问题思路二伪代码(空间优化版)
dp[0,...,W] = 0
for i = 1,...,N
    for j = W,...,w[i] // 必须逆向枚举!!!
        for k = [0, 1,..., min(n[i], j/w[i])]
            dp[j] = max(dp[j], dp[j−k*w[i]]+k*v[i])

总结

以上为背包问题的基本知识 详情可了解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值