目标和
LeetCode 494
对每一个数字都可以进行 + 操作 或者 - 操作,实际上最后就是一个树的搜索问题。
使用深度优先进行遍历,需要记录当前访问的是第几个元素,当前的结果。
递归结束的条件:
1)当前位置超过数组的边界
2)当前和等于目标和,记录结果
class Solution {
List<String> res = new ArrayList<>();
public int findTargetSumWays(int[] nums, int S) {
if (nums == null || nums.length == 0) {
return 0;
}
int num = dfs(nums, S, 0, 0, new StringBuilder());
for(String s :res){
System.out.println(s);
}
return num;
}
/**
*使用回溯,记录了每次操作的路径
*/
private int dfs(int[] nums, int target, int index, int curSum, StringBuilder sb) {
if (index == nums.length) {
if(curSum == target){
res.add(sb.toString());
return 1;
}
return 0;
}
String tmp = "+" + nums[index];
sb.append(tmp);
int left = dfs(nums, target, index + 1, curSum + nums[index], sb);
sb.delete(sb.length() - tmp.length(), sb.length());
tmp = "-" + nums[index];
sb.append(tmp);
int right = dfs(nums, target, index + 1, curSum - nums[index], sb);
sb.delete(sb.length() - tmp.length(), sb.length());
return left + right;
}
}
输出结果:
分割子集
LeetCode 416
0/1背包问题的变种
实际上就是要在数组中选出若干元素,使得它们的和等于数组所有元素和的一半。
分析
如果数组元素总和为奇数,不能进行划分。
为偶数则求是否存在元素之和为总和一半
dp[i][j] 表示 在前i件物品中进行选择,每个物品最多选择一次,是否能够凑出j
初始化:
有两种形式:
- int[][] dp = new int[len][target+1];
那么对第i件物品进行选择 下标就是i
dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i]]
初始化的值,应该用第1件物品进行初始化,dp[0][nums[0]] = true
对应的返回值 dp[len - 1][target];
- int[][] dp = new int[len+1][target+1];
那么对第i件物品进行选择 下标就是i-1
dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i-1]]
初始化的值,应该用所有物品都不选进行初始化,dp[0][0] = true
对应的返回值是dp[len][target]
class Solution {
public boolean canPartition1(int[] nums) {
int sum = 0;
int len = nums.length;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
if (sum % 2 != 0) {
return false;
}
int target = sum / 2;
boolean dp[][] = new boolean[len][target + 1];
//dp[i][j] 第 i件物品不选 = dp[i-1][j]
//第i件物品选 =dp[i][j-nums[i-1][]
if (nums[0] <= target) {
dp[0][nums[0]] = true;
}
for (int i = 1; i < len; i++) {
for (int j = 0; j <= target; j++) {
dp[i][j] = dp[i - 1][j];
boolean yes = j >= nums[i] ? dp[i - 1][j - nums[i]] : false;
dp[i][j] |= yes;
}
}
return dp[len - 1][target];
}
public boolean canPartition2(int[] nums) {
int sum = 0;
int len = nums.length;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
if (sum % 2 != 0) {
return false;
}
int target = sum / 2;
boolean dp[] = new boolean[target + 1];
//dp[i][j] 第 i件物品不选 = dp[i-1][j]
//第i件物品选 =dp[i][j-nums[i-1][]
dp[nums[0]] = true;
for (int i = 1; i < len; i++) {
for (int j = target; j >= 0; j--) {
boolean yes = j >= nums[i] ? dp[j - nums[i]] : false;
dp[j] |= yes;
}
}
return dp[target];
}
public boolean canPartition(int[] nums) {
int sum = 0;
int len = nums.length;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
if (sum % 2 != 0) {
return false;
}
int target = sum / 2;
boolean dp[][] = new boolean[len + 1][target + 1];
//dp[i][j] 第 i件物品不选 = dp[i-1][j]
//第i件物品选 =dp[i][j-nums[i-1][]
dp[0][0] = true;
// 这里 i能够取值取到len
for (int i = 1; i <= len; i++) {
for (int j = 0; j <= target; j++) {
dp[i][j] = dp[i - 1][j];
boolean yes = j >= nums[i - 1] ? dp[i - 1][j - nums[i - 1]] : false;
dp[i][j] |= yes;
}
}
return dp[len][target];
}
}
1和0
Leetcode 474
每个字符串都会贡献 a个0,b个1,求最多贡献m个0,n个1的最多的字符串个数
背包问题转换,背包容量 从一维的体积,变成两维度的约束,0,1的个数。
class Solution {
/***
* f[k][i][j] 代表考虑前 k 件物品,在数字 1 容量不超过 i,数字 0 容量不超过 j 的条件下的「最大价值」(每个字符串的价值均为 1)。
*
* f[k][i][j] = max(f[k - 1][i][j], f[k - 1][i - cnt[k][0]][j - cnt[k][1]] + 1)
* f[k][i][j]=max(f[k−1][i][j],f[k−1][i−cnt[k][0]][j−cnt[k][1]]+1)
* 其中 cntcnt 数组记录的是字符串中出现的 0101 数量
*
* @param strs
* @param m
* @param n
* @return
*/
public int findMaxForm(String[] strs, int m, int n) {
if (strs == null || strs.length == 0) {
return 0;
}
int len = strs.length;
int[][] arr = new int[len][2];
for (int i = 0; i < len; i++) {
String temp = strs[i];
int zero = 0;
int one = 0;
for (char c : temp.toCharArray()) {
if (c == '0') {
zero++;
} else {
one++;
}
}
arr[i] = new int[]{zero, one};
}
int[][][] dp = new int[len][m + 1][n + 1];
//初始化第0件物品 i代表 0的个数 j代表1的个数 (i,j)相当于背包容量
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
dp[0][i][j] = (i >= arr[0][0] && j >= arr[0][1]) ? 1 : 0;
}
}
for (int k = 1; k < len; k++) {
int zero = arr[k][0], one = arr[k][1];
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
//不选择第k件物品
int a = dp[k - 1][i][j];
//选择第k件物品 当前物品容量足以容纳第k件物品 就是在选择k-1的基础上+1 否则就是0
int b = (i >= zero && j >= one) ? dp[k - 1][i - zero][j - one] + 1 : 0;
dp[k][i][j] = Math.max(a, b);
}
}
}
return dp[len - 1][m][n];
}
}
完全背包
组合问题
Leetcode 377
在数组中选择一些数,可重复选择,构成target的排列数量
完全背包,但是要考虑物品的选择顺序。
先从dfs搜索开始:
// dp[i] 表示 从nums中任意进行选择,凑出i的方法数目
// 等同于 dp[i-nums[0]] + dp[i-nums[1]]+ dp[i-nums[2]] +.... + dp[i-nums[n-1]]
// 第一次可以从nums中任意选择一个数
public int dfs(int[] nums, int target, int curSum) {
if (curSum >= target) {
if (target == curSum) {
return 1;
}
return 0;
}
int res = 0;
for (int num : nums) {
res += dfs(nums, target, curSum + num);
}
return res;
}
上述操作需要穷举整个搜索树,但是有些数据是重复计算的,使用记忆化搜索来优化
int[] f;
//记忆化搜索
public int combinationSum4_1(int[] nums, int target) {
f = new int[target + 1];
//初始值全部填-1 代表该值没有计算过,
Arrays.fill(f, -1);
f[0] = 1;
return dfs2(nums, target, 0);
}
public int dfs2(int[] nums, int target, int curSum) {
if (curSum >= target) {
if (f[curSum] != -1) {
return f[curSum];
}
return 0;
}
int res = 0;
for (int num : nums) {
res += dfs(nums, target, curSum + num);
}
f[curSum] = res;
return res;
}
}
动态规划优化:
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];
//target=0,什么都不选是一种排列
dp[0] = 1;
//先遍历背包,再遍历物品
for (int j = 0; j <= target; j++) {
//当背包值固定时,此时可以选择的物品为i i:1~len,每次选择nums[i],剩下的j-nums[i]还在 1~len中去选
for (int i = 1; i <= len; i++) {
if (j >= nums[i - 1]) {
dp[j] += dp[j - nums[i - 1]];
}
}
}
return dp[target];
}
硬币找零问题
LeetCode322 找零的最少硬币数
每个物品可以选或者不选,每个硬币可以重复选择,是个完全背包问题。
public int coinChange1(int[] coins, int amount) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= amount; j++) {
//预先设置一个最大值
dp[i][j] = amount + 1;
}
}
//前i件物品 凑出价值为0 最少硬币数就是0 ,一个都不选,这个和方法数不一样
for (int i = 0; i <= n; i++) {
dp[i][0] = 0;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++) {
// 第i件物品不选
dp[i][j] = dp[i - 1][j];
if (j - coins[i - 1] >= 0) {
//dp[i] 第i件物品可以重复选
dp[i][j] = Math.min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
}
}
}
return dp[n][amount] == (amount + 1) ? -1 : dp[n][amount];
}
public int coinChange(int[] coins, int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp, n + 1);
//初始化
dp[0] = 0;
//先遍历背包,再遍历物品 完全背包
for (int j = 1; j <= n; j++) {
//固定背包重量,遍历物品
// 固定背包为j时,每次都是可以从[1,len]中选择硬币,选择最小的值,为此次背包的最小数
for (int i = 1; i <= coins.length; i++) {
int b = dp[j];
if (j >= coins[i - 1]) {
dp[j] = dp[j - coins[i - 1]] + 1;
}
dp[j] = Math.min(dp[j], b);
}
}
return dp[n] == n + 1 ? -1 : dp[n];
}
LeetCode 硬币的方法数
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
for (int i = 0; i <= n; i++) {
// 前i件物品 凑出质量是0的
dp[i][0] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++) {
dp[i][j] = dp[i - 1][j];
if (j - coins[i - 1] >= 0) {
// 从 第1到第i件物品中,选出并质量为j的方法数
//等于 选择第i件物品 的方法数 加上 不选择第i件物品 的方法数
dp[i][j] = dp[i][j] + dp[i][j - coins[i - 1]];
}
}
}
System.out.println(dp[n][amount]);
return dp[n][amount];
}
/***
* 使用空间压缩一维进行优化时
* 先遍历背包,再遍历物品
* 这样求出来的是排列数
* @param amount
* @param coins
* @return
*/
public int change2(int amount, int[] coins) {
// dp[i] 表示从第1个到第n个数中选择,和为i的方法数
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i >= coin) {
dp[i] += dp[i - coin];
}
}
}
return dp[amount];
}
/***
* 对于完全背包而言,如果是二维dp[i][j] 背包和物品的遍历顺序都可以
* 使用空间压缩一维进行优化时
* 先遍历物品,再遍历背包
* 这样求出来的是组合数
* @param amount
* @param coins
* @return
*/
public int change3(int amount, int[] coins) {
// dp[i] 表示从第1个到第n个数中选择,和为i的方法数
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int coin : coins) {
for (int i = 1; i <= amount; i++) {
if (i >= coin) {
dp[i] += dp[i - coin];
}
}
}
return dp[amount];
}
}
重点
change3方法中先遍历的是物品,再遍历的是背包,
固定前i个物品,然后遍历背包j,可以选择第i个物品,也可以不选择
dp[j] = dp[j] + dp[j-coin]
组合问题,不关心硬币的使用顺序,关注的是硬币是否被用到。
在前i个物品中进行选择,dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]] (i从0开始,表示第i件物品)前面i-1件凑出j 或者 前面i件凑出 j-coins[i-1] (还能再选择第i件)
对于change2方法,固定背包容量,物品可以随意选择,第一次选择的物品可以是任意位置的,具有顺序。
如果求组合数就是外层for循环遍历物品,内层for遍历背包;
如果求排列数就是外层for遍历背包,内层for循环遍历物品。