背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?它是在1978年由Merkel和Hellman提出的。
如下我分别总结了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[i−1][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[i−1][j],dp[i−1][j−w[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[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);
}
有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的情况下的最大价值
同上面的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的情况下的最大价值
给定数量不限的硬币,币值为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
说明:
注意:
你可以假设:
该题的思路属于完全背包问题(每一种硬币可以重复选择)+背包的方案数问题(即有多少种表示法)
我们同样需要进行动态规划思想进行求解,只是这里我们需要融合完全背包和方案数这两种思路
假设有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=0∑kdp[i−1,j−l∗w[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[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];