背包问题总结(涵盖0-1背包问题,完全背包问题,背包方案数问题等)

背包问题总结

背包问题介绍

背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?它是在1978年由Merkel和Hellman提出的。

如下我分别总结了0-1背包问题,完全背包问题,以及背包的方案数问题

1. 背包问题之0/1背包问题

在这里插入图片描述

题目描述

有N件物品和一个容量为C的背包,其中第i件物品的重量为w[i],价值为v[i],求解哪些物品装入背包中可以使得背包中物品的价值最大?注意:每个物品只能选择一次

问题分析:

0-1背包问题的特点就在于每件物品只能选择一次,故只存在选或者不选两种情况,对于传入N件物品,容量为C的参数:

我们定义w[N + 1],v[N + 1]: 存储物品的重量和价值
注意:我们定义w[N+1],v[N+1]的目的在于使得数组下标和第i个物品相对应,相一致,同样也可以直接定义w[N],v[N],只是此时w[i],v[i]表示为第i+1个物品的重量和价值
我们再定义dp[N + 1][C + 1]:其中dp[i][j]表示前i个物品可以选择的情况下,背包容量不超过j的最大价值
此时我们存在两个选择:
1)我们不选择第i个物品:dp[i][j] = dp[i - 1][j],即前i-1个物品,背包不超过j的最大价值
2)我们选择第i个物品:dp[i][j] = dp[i - 1][j - w[i]] + v[i]
即dp[i][j]为第i个物品的价值加上前i-1个物品,容量为j减去第i个物品的重量的最大价值之和
但是注意:如果我们选择第i个物品,则必须保证当前背包容量j要大于第i个物品的重量,即j > w[i]

在这里插入图片描述

由此可得前i个物品,背包容量不超过j的情况下的最大价值为:
i f   n o t   c h o o s e   w [ i ] : d p [ i ] [ j ] = d p [ i − 1 ] [ j ] if \space not \space choose \space w[i] : dp[i][j] = dp[i - 1][j] if not choose w[i]:dp[i][j]=dp[i1][j]

i f   c h o o s e   w [ i ] : d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] ) if \space choose \space w[i]: dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - w[i]] + v[i]) if choose w[i]:dp[i][j]=max(dp[i1][j],dp[i1][jw[i]]+v[i])

最终,前N个物品,背包容量不超过C的最大价值为:
d p [ N ] [ C ] dp[N][C] dp[N][C]

代码实现:
public class KnapsackProblems01 {
    /*
    * 01背包问题
    * N件物品和一个容量为V的背包:每件物品只能使用一次,第i件物品的体积是vi,价值为wi
    * 求解:将哪些物品装入背包,使得在不超过背包体积的情况下,总价值最大,输出最大价值
    * */
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        //n为输入的物品的个数
        int N = in.nextInt();
        //C为输入的背包的容量
        int C = in.nextInt();
        int[] w = new int[N + 1];//物品的重量
        int[] v = new int[N + 1];//物品的价值
        for (int i = 1; i <= N; i++) {
            w[i] = in.nextInt();
            v[i] = in.nextInt();
        }

        //定义二维的动态数组dp[][]
        //其中dp[i][j]:表示前i个物品,在不超过背包容量为j的情况下,得到的最大价值
        int[][] dp = new int[N + 1][C + 1];
        //给dp[0][j],dp[i][0]赋值初始值0:表示没有物品选择情况下,以及背包容量为0的情况下,最大价值均为0
        for (int i = 0; i <= N; i++) {
            dp[i][0] = 0;
        }
        for (int i = 0; i <= C; i++) {
            dp[0][i] = 0;
        }
        //进行dp[i][j]的赋值操作
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= C; j++) {
                //不选择第i个物品的情况下,dp[i][j] = dp[i - 1][j]
                dp[i][j] = dp[i - 1][j];
                //如果第i个物品的重量v[i] <= j,则选择第i个物品,dp[i][j] = dp[i - 1][j - w[i]] + v[i]
                //此时最大价值为dp[i][j] = Math.max(dp[i][j],dp[i - 1][j - w[i]] + v[i]);
                if (w[i] <= j) {
                    dp[i][j] = Math.max(dp[i][j],dp[i - 1][j - w[i]] + v[i]);
                }
            }
        }
        //此时dp[N][C]即为选择N个物品时,背包的最大价值
        int res = dp[N][C];
        System.out.println("该背包在" + N + "个物品中不重复的挑选,不超过背包总容量的情况下,最大价值为 :" + res);      
}

将二维dp优化为一维dp数组实现,实现动态规划的降维
将二维dp数组降维到一维dp数组:
由于dp[i][j]只和dp[i-1][j]的状态有关,故我们定义f[C+1],其中f[j]表示当前遍历的前j个物品可以选择,背包容量为j的情况下的最大价值
将dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - w[i]] + v[i]),将二维的i和i-1都去除掉,但是我们需要保证后面的是i - 1的状态:
即dp[j] = max(dp[j],dp[j - w[i]] + v[i])
故我们选择内循环从C --> w[i]进行循环:这样上面公式前面的dp[j]是前i个物品,背包为j的最大价值,后面的dp[j]和dp[j - w[i]]为前i - 1个物品,背包为j的最大价值
//优化0-1背包问题代码:定义一维数组f[N+1]:其中f[j]表示前i个物品背包总容量为j的情况下的最大价值
        int[] f =  new int[C + 1];
        f[0] = 0;
        for (int i = 1; i <= N; i++) {
            for (int j = C; j >= w[i]; j--) {
                //j从C到1:使得下面的f[j]是f[i - 1][j]和f[i - 1][j - w[i]] + v[i]的最大值
                f[j] = Math.max(f[j],f[j - w[i]] + v[i]);
            }
        }
        //此时的f[C]是选择N个物品时,背包的最大价值
        int result = f[C];
        System.out.println("该背包在" + N + "个物品中不重复的挑选,不超过背包总容量的情况下,最大价值为 :" + result);
    }

2. 背包问题之完全背包问题

题目描述

有N件物品和一个容量为C的背包,其中第i件物品的重量为w[i],价值为v[i],求解哪些物品装入背包中可以使得背包中物品的价值最大?注意:每个物品可以重复选择

即题目的基础条件和前面的0-1背包问题相同,只是每件物品可以重复选择

题目分析和代码实现

我们仍然先选择使用二维的dp数组进行求解

我们定义dp[N + 1][C + 1]:
其中dp[i][j]表示前i个物品可以选择的情况下,背包容量不超过j的最大价值
此时我们仍然存在两种选择:
1)不选择第i个物品:dp[i][j] = dp[i - 1][j]
2)选择第i个物品:此时由于存在可以重复选择的情况故我们可以重复进行选择,即此时可以选择1,2,3...k次(1< k <= j/w[i]),:
for(int k = 1;k * w[i] <= j;k++) {
    dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - k * w[i]] + k * v[i]);
}
故我们联合上面两种选择情况,核心代码为:
for(int i = 1;i <= N;i++) {
    for(int j = 1;j <= C;j++) {
        dp[i][j] = dp[i - 1][j];
        for(int k = 1;k * w[i] <= j;k++) {
            dp[i][j] = max(dp[i][j],dp[i - 1][j - k * w[i]] + k * v[i]);
        }
    }
}
最后的dp[N][C]即为前N个物品,背包容量为C的情况下的最大价值
优化为一维dp数组
同上面的0-1降维思想相同,由于前i种物品的状态只和前i-1种物品的状态有关,故我们可以选择进行降维,保证降维以后的一维数组dp[j],前者为前i个物品的最大价值,后者为前i-1个物品的最大价值
代码实现:
int[] dp = new int[C + 1];
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])
    }
}
最后的dp[C]即为前N个物品,背包容量为C的情况下的最大价值

3. 背包问题之背包的方案数问题-----抛硬币问题(完全背包问题+背包方案数的结合问题讲解)

问题描述

给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。(结果可能会很大,你需要将结果模上1000000007)

在这里插入图片描述

示例1:

 输入: n = 5
 输出:2
 解释: 有两种方式可以凑成总金额:
 5=5
 5=1+1+1+1+1

示例2:

 输入: n = 10
 输出:4
 解释: 有四种方式可以凑成总金额:
 10=10
 10=5+5
 10=5+1+1+1+1+1
 10=1+1+1+1+1+1+1+1+1+1

说明:

注意:

你可以假设:

  • 0 <= n (总金额) <= 1000000
问题分析:
该题的思路属于完全背包问题(每一种硬币可以重复选择)+背包的方案数问题(即有多少种表示法)
我们同样需要进行动态规划思想进行求解,只是这里我们需要融合完全背包和方案数这两种思路
假设有m个不同的硬币面值数可以选择,凑成的总金额为n,:
定义数组w[m + 1]: w[i]表示第i种硬币的硬币面值数
我们首先定义二维数组dp[m+1][n+1]:其中dp[i][j]表示前i种硬币面值可以选择的情况下,构成面值总金额为j的方案数
1)当j = 0:即表示构成面值总金额为0的方案数,则只存在一种,即所有的面值硬币都不选择,故dp[i][0] = 1,i:0 ~ m
2)当不选择第i个物品时,则dp[i][j] = dp[i - 1][j] = dp[i - 1][j - 0 * w[i]]
3)当选择第i个物品时,则由于可以重复进行选择
dp[i][j] = dp[i - 1][j - 1 * w[i]] + dp[i - 1][j - 2 * w[i]] + ...+ dp[i - 1][j - k * w[i]],其中k:1 ~ j/w[i]
即dp[i][j]等于选择1个当前硬币面值,2个硬币面值...,k个硬币面值的方案数之和

d p [ i ] [ [ j ] = ∑ l = 0 k d p [ i − 1 , j − l ∗ w [ i ] ] , k ∈ ( 1 , j / w [ i ] ) dp[i][[j] = \sum_{l=0}^k dp[i - 1,j - l *w[i]],k\in(1,j/w[i]) dp[i][[j]=l=0kdp[i1,jlw[i]],k(1,j/w[i])

核心代码:
static final int MOD = 1000000007;
int [][] dp = new  int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
    dp[i][0] = 1;
}
for (int i = 1; i <= m; i++) {
    for (int j = 1; j <= n; j++) {
        //不放入第i个物品
        dp[i][j] = dp[i- 1][j] % MOD;
        //放入第i个物品
        for (int k = 1; k * w[i] <= j; k++) {
            dp[i][j] = (dp[i][j] + dp[i - 1][j - k * w[i]]) % MOD;
        }
    }
}
return dp[m][n];
优化为一维dp数组:
由于dp[i][j]只和dp[i - 1][j]的状态存在关联,故我们可以选择进行降维处理
我们定义dp[n + 1]数组:其中dp[j]表示构成面值总金额为j的方案数
则由于上面的动态转移方程,我们修改后的核心代码如下:
static final int MOD = 1000000007;
int[] dp = new int[n + 1];
dp[0] = 1; //表示前i个物品构成面值为0的方案数为1
for (int i = 1; i <= m; i++) {
    for (int j = w[i]; j <= n; j++) {
        dp[j] = (dp[j] + dp[j - w[i]]) % MOD;
    }
}
return dp[n];
  • 9
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值