有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值 。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。
- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。
第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。
// W 为背包总体积// N 为物品数量// weights 数组存储 N 个物品的重量// values 数组存储 N 个物品的价值public int knapsack(int W, int N, int[] weights, int[] values) {int[][] dp = new int[N + 1][W + 1];for (int i = 1; i <= N; i++) {int w = weights[i - 1], v = values[i - 1];for (int j = 1; j <= W; j++) {if (j >= w) {dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v);} else {dp[i][j] = dp[i - 1][j];}}}return dp[N][W];}
空间优化
在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
public int knapsack(int W, int N, int[] weights, int[] values) {int[] dp = new int[W + 1];for (int i = 1; i <= N; i++) {int w = weights[i - 1], v = values[i - 1];for (int j = W; j >= 1; j--) {if (j >= w) {dp[j] = Math.max(dp[j], dp[j - w] + v);}}}return dp[W];}
0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22.
变种
- 完全背包:物品数量为无限个
- 多重背包:物品数量有限制
- 多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制
- 其它:物品之间相互约束或者依赖
区别
- 0-1背包:有N件物品和一个容量为V的背包,第i件物品消耗的容量为Ci,价值为Wi,求解放入哪些物品可以使得背包中总价值最大。两层for循环,外层为第i个物体,内层为容量。
- 完全背包:有N种物品和一个容量为V的背包,每种物品都有无限件可用,第i件物品消耗的容量为Ci,价值为Wi,求解放入哪些物品可以使得背包中总价值最大。两层for循环,外层为容量,内层为第i个物体。或转化为0-1背包,三层for循环。
- 多重背包:有N种物品和一个容量为V的背包,第i种物品最多有Mi件可用,每件物品消耗的容量为Ci,价值为Wi,求解入哪些物品可以使得背包中总价值最大。
状态方程
0-1背包的状态方程:Max{dp[i-1,v], dp[i-1,v-Ci]+w1}
完全背包的状态方程:Max{dp[i-1,v-kCi]+kWi | 0<=kCi<=v}
多重背包的状态方程:Max{dp[i-1,c-kCi]+kWi | 0<=k<=Mi}
三种背包问题都是基于子问题来选取价值最大的一个,只是选择的范围不一样。
416. Partition Equal Subset Sum (Medium)
题目描述
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
思路
背包总容量为总和的一半,物体为数组中的每一个数,重量为数的大小,价值为是否等于该重量(true/false)
1.明确dp定义:dp[i][j]表示前i个数是否能组合等于j值。
2.定义base case: dp[0][j]=1, dp[i][0]=0
3.状态转移:dp[i][j] = d[i-1][j] | d[i-1][j-nums[i-1]]
class Solution {public boolean canPartition(int[] nums) {if(nums==null || nums.length<2){return false;}int len = nums.length;int sum = 0;for(int i=0; i<len; i++){sum = sum + nums[i];}if(sum%2!=0){return false;}int weight = sum/2;//初始化boolean[][] dp = new boolean[len+1][weight+1];for(int i=0; i<weight+1; i++){dp[0][i] = false;}for(int i=0; i<len+1; i++){dp[i][0] = true;}//状态转移for(int i=1; i<len+1; i++){for(int j=1; j<weight+1; j++){dp[i][j] = dp[i-1][j];if(j>=nums[i-1]){dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i-1]];}}}return dp[len][weight];}}
内存优化
前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],防止将 dp[i-1][j-w] 覆盖,倒序来循环求解。
class Solution {public boolean canPartition(int[] nums) {if(nums==null || nums.length<2){return false;}int len = nums.length;int sum = 0;for(int i=0; i<len; i++){sum = sum + nums[i];}if(sum%2!=0){return false;}int weight = sum/2;//初始化boolean[] dp = new boolean[weight+1];dp[0] = true;//状态转移for(int i=1; i<len+1; i++){for(int j=weight; j>0; j--){if(j>=nums[i-1]){dp[j] = dp[j] | dp[j-nums[i-1]];}}}return dp[weight];}}
494. Target Sum (Medium)
题目描述
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例
输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。
思路
1.明确dp定义:dp[i][j]表示前i项和等于j的方法数。
2.base case:dp[0][j(-/+)nums[0]]+=1;
3.状态转移:在数组成立的情况下:dp[i][j-minsum] = dp[i-1][p1-minsum]+ dp[i-1][p2-minsum];
class Solution {public int findTargetSumWays(int[] nums, int S) {int maxsum = 0;int minsum = 0;for(int i=0; i<nums.length; i++){if(nums[i]>=0){maxsum += nums[i];minsum -= nums[i];}else{maxsum -= nums[i];minsum += nums[i];}}if(S<minsum || S>maxsum){return 0;}int len = nums.length;int[][] dp = new int[len][maxsum-minsum+1];//初始化dp[0][nums[0]-minsum]+=1;dp[0][-nums[0]-minsum]+=1;for(int i=1; i<len; i++){for(int j=minsum; j<=maxsum; j++){int p1 = j-nums[i];int p2 = j+nums[i];if(p1>=minsum && p1<=maxsum){dp[i][j-minsum] += dp[i-1][p1-minsum];}if(p2>=minsum && p2<=maxsum){dp[i][j-minsum] += dp[i-1][p2-minsum];}}}return dp[len-1][S-minsum];}}
另一种思路
该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。
可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导:
sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
2 * sum(P) = target + sum(nums)
因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。
copy 上题的解法:
public int findTargetSumWays(int[] nums, int S) {int sum = computeArraySum(nums);if (sum < S || (sum + S) % 2 == 1) {return 0;}int W = (sum + S) / 2;int[] dp = new int[W + 1];dp[0] = 1;for (int num : nums) {for (int i = W; i >= num; i--) {dp[i] = dp[i] + dp[i - num];}}return dp[W];}//求和private int computeArraySum(int[] nums) {int sum = 0;for (int num : nums) {sum += num;}return sum;}
474. Ones and Zeroes (Medium)
题目描述
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,”0001”,”1”,”0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,”1”} 和 {“10”,”1”,”0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
class Solution {public int findMaxForm(String[] strs, int m, int n) {int len = strs.length;List<int[]> list = new ArrayList<>();list = countString(strs);int[][][] dp = new int[len+1][m+1][n+1];for(int x=1; x<len+1; x++){int[] tmp = list.get(x-1);for(int i=0; i<m+1; i++){for(int j=0; j<n+1; j++){dp[x][i][j] = dp[x-1][i][j];if(tmp[0]<=i && tmp[1]<=j){dp[x][i][j] = Math.max(dp[x][i][j], dp[x-1][i-tmp[0]][j-tmp[1]] + 1);}}}}return dp[len][m][n];}public List<int[]> countString(String[] strs){List<int[]> list = new ArrayList<>();int[] tmp;for(int i=0; i<strs.length; i++){String s = strs[i];tmp = new int[2];for(int j=0; j<s.length(); j++){char c = s.charAt(j);if(c=='0'){tmp[0]++;}else{tmp[1]++;}}list.add(tmp);}return list;}}
内存优化
class Solution {public int findMaxForm(String[] strs, int m, int n) {int len = strs.length;List<int[]> list = new ArrayList<>();list = countString(strs);int[][] dp = new int[m+1][n+1];for(int x=0; x<len; x++){int[] tmp = list.get(x);for(int i=m; i>=0; i--){for(int j=n; j>=0; j--){if(i>=tmp[0] && j>=tmp[1]){dp[i][j] = Math.max(dp[i][j], dp[i-tmp[0]][j-tmp[1]]+1);}}}}return dp[m][n];}public List<int[]> countString(String[] strs){List<int[]> list = new ArrayList<>();int[] tmp;for(int i=0; i<strs.length; i++){String s = strs[i];tmp = new int[2];for(int j=0; j<s.length(); j++){char c = s.charAt(j);if(c=='0'){tmp[0]++;}else{tmp[1]++;}}list.add(tmp);}return list;}}
322. Coin Change (Medium)-完全背包
题目描述
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
思路
注意硬币数量无限。dp[i] = Math.min(min, dp[i-money]+1);
class Solution {public int coinChange(int[] coins, int amount) {int len = coins.length;int[] dp = new int[amount+1];Arrays.fill(dp,-1);dp[0] = 0;for(int i=1; i<=amount; i++){int min = Integer.MAX_VALUE;for(int j=0; j<len; j++){int money = coins[j];if(i>=money && dp[i-money]!=-1){min = Math.min(min, dp[i-money]+1);}}if(min<Integer.MAX_VALUE){dp[i] = min;}}return dp[amount];}}
完全背包同一格式版
class Solution {public int coinChange(int[] coins, int amount) {int[] dp = new int[amount+1];Arrays.fill(dp,Integer.MAX_VALUE);dp[0] = 0;for(int coin:coins){for(int i=coin; i<=amount; i++){if(dp[i-coin]!=Integer.MAX_VALUE){dp[i] = Math.min(dp[i], dp[i-coin]+1);}}}if(dp[amount]==Integer.MAX_VALUE) {return -1;}else {return dp[amount];}}}
518. Coin Change 2 (Medium)
题目描述
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
思路
完全背包,将第二层循环的倒序改为正序。
class Solution {public int change(int amount, int[] coins) {int len = coins.length;int[] dp = new int[amount+1];dp[0] = 1;for(int money:coins){for(int i=money; i<=amount; i++){dp[i] += dp[i-money];}}return dp[amount];}}
139. Word Break (Medium)
题目描述
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
思路
完全背包问题,但是该问题涉及到字典中单词的使用顺序,也就是说物品必须按一定顺序放入背包中。求解顺序的完全背包问题时,对物品的迭代应该放在最里层,对背包的迭代放在外层,只有这样才能让物品按一定顺序放入背包中。
class Solution {public boolean wordBreak(String s, List<String> wordDict) {boolean[] dp = new boolean[s.length()+1];dp[0] = true;for(int i=0; i<s.length()+1; i++) {for(int j=0; j<wordDict.size(); j++) {String ref = wordDict.get(j);int len = ref.length();if(i>=len) {String tmp = s.substring(i-len, i);if(ref.equals(tmp) && dp[i-len]) {dp[i] = true;}}}}return dp[s.length()];}}
377. Combination Sum IV (Medium)
题目描述
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例
nums = [1, 2, 3]
target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
思路
同上,涉及顺序的完全背包。
class Solution {public int combinationSum4(int[] nums, int target) {if(nums==null ||nums.length==0){return 0;}int len = nums.length;int[] dp = new int[target+1];dp[0] = 1;for(int i=1; i<target+1; i++){for(int num : nums){if(i>=num && dp[i-num]!=-1){dp[i] += dp[i-num];}}}return dp[target];}}
结果 
