Leetcode——分割等和子集 / 目标和 (01背包 / DP)

Posted Yawn,

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Leetcode——分割等和子集 / 目标和 (01背包 / DP)相关的知识,希望对你有一定的参考价值。

1. 分割等和子集

(0)背包问题

与494. 目标和 类似,属于01背包问题,可以把问题抽象为“给定一个数组和一个容量为x的背包,求有多少种方式让背包装满(有多少种子集能让子集之和等于背包容量"

附上01背包问题的模版:

//01背包
for (int i = 0; i < n; i++) {
    for (int j = m; j >= V[i]; j--) {
        f[j] = max(f[j], f[j-V[i]] + W[i]);
    }
}
//完全背包
for (int i = 0; i < n; i++) {
    for (int j = V[i]; j <= m; j++) {
        f[j] = max(f[j], f[j-V[i]] + W[i]);
    }
}
  • f[j] 代表当前背包容量为 j 的时候,可以获取的最大价值。
  • 完全背包是从左向右遍历,f[j-V[i]]取到的是拿第i个物品时的值,是新值,可以重复无限的拿,f[j]的值也会随之增加。
    V:商品的体积
    W:商品的价值

(1)DP

  • 解决的基本思路是:物品一个一个选,容量也一点一点增加去考虑,这一点是「动态规划」的思想,特别重要。
  • 具体做法是:画一个 len 行,target + 1 列的表格。这里 len 是物品的个数,target 是背包的容量。len 行表示一个一个物品考虑,target + 1多出来的那 1 列,表示背包容量从 0 开始考虑。很多时候,我们需要考虑这个容量为 0 的数值。

状态转移方程:

  • 状态定义:dp[i][j] 表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j。
  • 状态转移方程:很多时候,状态转移方程思考的角度是「分类讨论」,对于「0-1 背包问题」而言就是「当前考虑到的数字选与不选」。
    • 选择 nums[i],如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;
    • 选择 nums[i],如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。
状态转移方程:
	dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]

初始化考虑:

  • j - nums[i] 作为数组的下标,一定得保证大于等于 0 ,因此 nums[i] <= j;
  • 注意到一种非常特殊的情况:j 恰好等于 nums[i],即单独 nums[j] 这个数恰好等于此时「背包的容积」 j,这也是符合题意的。
  • 因此完整的状态转移方程是:

  • 初始化:dp[0][0] = false,因为候选数 nums[0] 是正整数,凑不出和为 0;
  • 输出:dp[len - 1][target],这里 len 表示数组的长度,target 是数组的元素之和(必须是偶数)的一半。
public class Solution {

    public boolean canPartition(int[] nums) {
        int len = nums.length;
        // 题目已经说非空数组,可以不做非空判断
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 特判:如果是奇数,就不符合要求
        if ((sum & 1) == 1) {
            return false;
        }

        int target = sum / 2;
        // 创建二维状态数组,行:物品索引,列:容量(包括 0)
        //定义dp,dp[i][j]表示nums[0:i+1]中是否存在一些元素的和等于j+1
        boolean[][] dp = new boolean[len][target + 1];

        // 先填表格第 0 行,第 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];

                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[len - 1][target];
    }
}

(2)DP(优化)

递推公式:dp[i] = dp[i] + dp[i-num] ,对于当前的第i个物品,有拿和不拿两种情况,dp[i]表示不拿的情况,dp[i-num]表示拿的情况,因此要将两者相加

class Solution {
    public boolean canPartition(int[] nums) {
        int len = nums.length;
        int sum = 0;
        for (int n : nums) {
            sum += n;
        }

        //整数相加不可能得小数
        if (sum % 2 != 0)
            return false;
        
        //分割子集的和
        int W = sum / 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] 
                //对于当前的第i个物品,有拿和不拿两种情况,dp[i]表示不拿的情况,dp[i-num]表示拿的情况,因此要将两者相加
                dp[i] += dp[i - num];
            }
        }
        return dp[W] != 0;
    }
}

2 . 目标和

(1)DFS


数据范围只有 20不大,而且每个数据只有 +/-+/− 两种选择,因此可以直接使用 DFS 进行「爆搜」。
而 DFS 有「使用全局变量维护」和「接收返回值处理」两种形式。

接收返回值处理:
u: 数值下标
cur: 当前结算结果

class Solution {
    public int findTargetSumWays(int[] nums, int t) {
        return dfs(nums, t, 0, 0);
    }
    int dfs(int[] nums, int t, int u, int cur) {
    	//串联完所有整数
        if (u == nums.length) {
            return cur == t ? 1 : 0;
        }
        int left = dfs(nums, t, u + 1, cur + nums[u]);
        int right = dfs(nums, t, u + 1, cur - nums[u]);
        return left + right;
    }
}

使用全局变量维护:

class Solution {
    int ans = 0;
    public int findTargetSumWays(int[] nums, int t) {
        dfs(nums, t, 0, 0);
        return ans;
    }
    void dfs(int[] nums, int t, int u, int cur) {
	    //串联完所有整数
        if (u == nums.length) {
            ans += cur == t ? 1 : 0;
            return;
        }
        dfs(nums, t, u + 1, cur + nums[u]);
        dfs(nums, t, u + 1, cur - nums[u]);
    }
}

(2)记忆化DFS

  • DFS 的函数签名中只有「数值下标 u」和「当前结算结果 cur」为可变参数,考虑将其作为记忆化容器的两个维度,返回值作为记忆化容器的记录值。
class Solution {
    public int findTargetSumWays(int[] nums, int t) {
        return dfs(nums, t, 0, 0);
    }
    Map<String, Integer> cache = new HashMap<>();
    int dfs(int[] nums, int t, int u, int cur) {
        String key = u + "_" + cur;
        if (cache.containsKey(key)) return cache.get(key);
        if (u == nums.length) {
            cache.put(key, cur == t ? 1 : 0);
            return cache.get(key);
        }
        int left = dfs(nums, t, u + 1, cur + nums[u]);
        int right = dfs(nums, t, u + 1, cur - nums[u]);
        cache.put(key, left + right);
        return cache.get(key);
    }
}

(3)DP

class Solution {
    public int findTargetSumWays(int[] nums, int t) {
        int n = nums.length;
        int s = 0;
        for (int i : nums) 
        	s += Math.abs(i);
        if (t > s) 
        	return 0;
        int[][] f = new int[n + 1][2 * s + 1];
        f[0][0 + s] = 1;
        
        for (int i = 1; i <= n; i++) {
            int x = nums[i - 1];
            for (int j = -s; j <= s; j++) {
                if ((j - x) + s >= 0) 
                	f[i][j + s] += f[i - 1][(j - x) + s];
                
                if ((j + x) + s <= 2 * s) 
                	f[i][j + s] += f[i - 1][(j + x) + s];
            }
        }
        return f[n][t + s];
    }
}

以上是关于Leetcode——分割等和子集 / 目标和 (01背包 / DP)的主要内容,如果未能解决你的问题,请参考以下文章

*Leetcode 416. 分割等和子集

leetcode 416. 分割等和子集---直接解法

leetcode 416. 分割等和子集

Leetcode刷题Python416. 分割等和子集

LeetCode 416. 分割等和子集

LeetCode 416. 分割等和子集 c++/java详细题解