动态规划知识参考:
Java数据结构与算法——动态规划
背包问题是一类比较特殊的动态规划问题,我们还是使用之前提到的解动态规划问题的四个步骤来思考这类问题。
背包类动态规划问题和其他的动态规划问题的不同之处在于,背包类动态规划问题会选用值来作为动态规划的状态,而我们之前讨论的动态规划问题,基本上都是利用数组或者字符串的下标来表示动态规划的状态。
针对背包类问题,我们可以画表格来辅助我们思考问题。背包类问题有基本的雏形,题目特征明显,当你理解了这类问题的解法后,遇到类似问题基本上不需要额外的辅助就可以给出大致的解法。
题目描述:
有 N 件物品和一个容量为 V 的背包。第 i 件物品的体积是 C[i],价值是 W[i]。求解将哪些物品装入背包可使价值总和最大,求出最大总价值。
我们按照前面的动态规划的四个步骤来分析:
我们要求解的问题是 “背包能装入物品的总价值最大”,影响这个问题的两个因素是:背包的容量大小和物品的属性(大小和价值)。对于物品来说,有放入背包和不放入背包两种结果。
我们举个例子,画表格来分析一下。假设背包的大小是 10,有 4 个物品,体积分别是 [2,3,5,7],价值分别是 [2,5,2,5]。
items\volume | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
items\volume | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | 0 | 2 | 5 | 5 | 7 | 7 | 7 | 7 | 7 | 7 |
items\volume | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | 0 | 2 | 5 | 5 | 7 | 7 | 7 | 7 | 7 | 7 |
3 | 0 | 0 | 2 | 5 | 5 | 7 | 7 | 7 | 7 | 7 | 9 |
items\volume | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | 0 | 2 | 5 | 5 | 7 | 7 | 7 | 7 | 7 | 7 |
3 | 0 | 0 | 2 | 5 | 5 | 7 | 7 | 7 | 7 | 7 | 9 |
4 | 0 | 0 | 2 | 5 | 5 | 7 | 7 | 7 | 7 | 7 | 10 |
由此,我们就根据物品和体积将问题拆分成子问题,也就是 “前 n 个物品在体积 V 处的最大价值” 可以由 “前 n - 1 个物品的情况” 推导得到。
由问题拆解我们找到了第 i 个问题和第 i - 1 个问题的联系,我们定义 dp[i][j] 表示:考虑将前 i 个物品放入体积为 j 的背包里能够获得的最大价值。
对于第 i 个物品,我们有放入背包和不放入背包两种选择。但是要注意,不放入背包也有两种情况:一种是因为背包空间不足,放不下第 i 个物品;另一种是因为放入第 i 个物品后的最大价值小于不放入第 i 个物品的最大价值。因此
数组初始化:
测试代码:
public class Test {
public static int zeroOnePack(int V, int[] C, int[] W){
if(V <= 0 || C.length != W.length)
return 0;
int n = C.length;
int[][] dp = new int[n + 1][V + 1];
for(int i = 0; i <= n; i++){
for(int j = 0; j <= V; j++){
if(i == 0 || j == 0){
dp[i][j] = 0;
}else{
// 这里注意索引的变化:C[i-1]表示第 i 个物品。
if(j < C[i - 1])// 背包放不下第 i 个物品
dp[i][j] = dp[i - 1][j];
else// 背包能放下第 i 个物品,选择价值最大的方案
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - C[i - 1]] + W[i - 1]);
}
}
}
return dp[n][V];
}
public static void main(String[] args){
int[] C = {2, 3, 5, 7};
int[] W = {2, 5, 2, 5};
System.out.println(zeroOnePack(10, C, W));
}
}
输出:
10
空间优化:
我们发现,第 i 个问题的状态只依赖于第 i - 1 个问题的状态,也就是 dp[i][…] 只依赖于 dp[i - 1][…],另外一点就是当前考虑的背包体积只会用到比其体积小的物品。基于这些信息,我们状态数组的维度可以少开一维,但是遍历的方向上需要从后往前遍历,从而保证子问题需要用到的数据不被覆盖,优化版本如下:
public static int zeroOnePackOpt(int V, int[] C, int[] W){
if(V <= 0 || C.length != W.length)
return 0;
int n = C.length;
int[] dp = new int[V + 1];
dp[0] = 0;// 背包为空时,价值为 0
for(int i = 0; i < n; i++){
for(int j = V; j >= C[i]; j--){
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
return dp[V];
}
因为物品只能被选中 1 次,或者被选中 0 次,所以我们称这种背包问题为 01 背包问题。
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
题目中说把数组分为两个子集,且两个子集的元素和相等。那就是说,每个子集的和等于数组所有元素的和的一半,这就要求数组的和一定是偶数,这可以当作一个特判。那我们就很容易想到,从数组中找出一部分元素,让它们的和等于数组元素总和的一半。
回顾 01 背包问题,实际上就是在一堆物品中找出一部分物品,这一部分物品的价值最大。所以我们就可以用 01 背包问题的求解过程来分析该问题。
参考代码:
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
if(n == 0)
return false;
int sum = 0;
for(int i : nums)
sum += i;
// 数组元素总和为偶数才可以分割
if(sum % 2 == 1)
return false;
int target = sum / 2;
// dp[i][j]表示从数组的[0,i]区间内找到一些元素,使得它们的和等于j
boolean[][] dp = new boolean[n][target + 1];
// 第 1 个数只能让容积为它自己的背包恰好装满
if(nums[0] <= target)
dp[0][nums[0]] = true;
for(int i = 1; i < n; i++){
for(int j = 0; j <= target; j++){
dp[i][j] = dp[i - 1][j];
// j 恰好等于 nums[i],即单独 nums[j] 这个数恰好等于此时“背包的容积” j
if(nums[i] == j){
dp[i][j] = true;
continue;
}
if(nums[i] < j){
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[n - 1][target];
}
}
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
题目给定了一个硬币数组 coins 和一个总金额 amount,结合背包问题,总金额就可以看成背包的体积,硬币的面额可以看成背包问题中物品的体积。背包问题中还有物品的价值这一属性,这里我们可以把每一块硬币的价值看为 1,问题要求凑成总金额的硬币数最少,也就是 “填满背包的最小价值”。
参考代码:
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for(int i = 1; i <= amount; i++){
for(int j = 0; j < coins.length; j++){
if(i >= coins[j] && dp[i - coins[j]] != amount + 1)
dp[i] = Math.min(dp[i], 1 + dp[i - coins[j]]);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
}