数据结构与算法篇-子集和计数问题

子集和问计数问题

问题:给定一组数字,能否选出一个子集,使其和等于目标值,如果能,请计算不同的方法数。如果不能,请返回 0.

第 1 步:定义状态

dp[i][w]表示使用数组前i个元素凑成和w的不同方法总数。

第 2 步:推导递推关系

计算 dp[i][w] 时,我们考虑第 i 个元素 nums[i-1]

有两个选择:

选择 1:不选 nums[i-1]

  • 方法数:dp[i-1][w]

选择 2:选nums[i-1]

  • 前提:w >= nums[i-1]
  • 方法数:dp[i-1][w - nums[i-1]]

合并后得到:

txt 复制代码
dp[i][w] = dp[i-1][w]
         + (w >= nums[i-1] ? dp[i-1][w - nums[i-1]] : 0)

朴素递归解

java 复制代码
public static int findSubsetSumWaysRecursion(int[] nums, int target) {
    return findSubsetSumWaysRecursionHelper(nums, target, nums.length);
}

private static int findSubsetSumWaysRecursionHelper(int[] nums, int remainSum, int i) {
    if (i == 0) {
        if (remainSum == 0) {
            return 1;
        }else {
            return 0;
        }
    }
    int count = findSubsetSumWaysRecursionHelper(nums, remainSum, i - 1);
    if (remainSum >= nums[i-1]) {
        count += findSubsetSumWaysRecursionHelper(nums, remainSum - nums[i-1], i - 1);
    }
    return count;
}

记忆化

java 复制代码
public static int findSubsetSumWaysMemo(int[] nums, int target) {
    int n = nums.length;
    int[][] memo = new int[n+1][target+1];
    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= target; j++) {
            memo[i][j] = -1;
        }
    }
    return findSubsetSumWaysMemoHelper(nums, target, nums.length, memo);
}

private static int findSubsetSumWaysMemoHelper(int[] nums, int remainSum, int i, int[][] memo) {
    if (i == 0) {
        if (remainSum == 0) {
            return 1;
        }else {
            return 0;
        }
    }

    if (memo[i][remainSum] != -1) {
        return memo[i][remainSum];
    }

    int count = findSubsetSumWaysMemoHelper(nums, remainSum, i - 1, memo);
    if (remainSum >= nums[i-1]) {
        count += findSubsetSumWaysMemoHelper(nums, remainSum - nums[i-1], i - 1, memo);
    }
    memo[i][remainSum] = count;
    return count;
}

制表法

java 复制代码
public static int findSubsetSumWaysTabulation(int[] nums, int target) {
    int n = nums.length;
    int[][] dp = new int[n+1][target+1];

    for (int i = 0; i <= n; i++) {
        dp[i][0] = 1;
    }

    for (int j = 0; j <= target; j++) {
        dp[0][j] = 0;
    }
    
    dp[0][0] = 1;

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= target; j++) {
            dp[i][j] = dp[i-1][j];
            if (j >= nums[i-1]) {
                dp[i][j] += dp[i-1][j-nums[i-1]];
            }
        }
    }


    return dp[n][target];
}

空间优化法

复制代码
public static int findSubsetSumWaysOptimized(int[] nums, int target) {
    int n = nums.length;
    int[] dp = new int[target+1];

    dp[0] = 1;

    for (int i = 1; i <= n; i++) {
        for (int j = target; j >= nums[i-1]; j--) {
            dp[j] += dp[j-nums[i-1]];
        }
    }

    return dp[target];
}

应用

可应用子集和计数问题来求解目标和计数问题

示例代码

java 复制代码
public static int findTargetSumWays(int[] nums, int target) {
    int sum = Arrays.stream(nums).sum();
    int subTarget = (target + sum) / 2;
    return SubsetSum.findSubsetSumWaysOptimized(nums, subTarget);
}

参考资料