# 目标和

题目链接🔗

结果 target 可以将数组分为两部分,左组合 left - 右组合 right = target。

left + right = sum,而数组总和是固定的,因此 right = sum - left。

那么 left - (sum - left) = target,left = (target + sum) / 2。

target 和 sum 是固定值,那么问题就转化为在数组中求 left 的组合。

转化为背包问题就是,装满容量为 left 的背包,有几种方法。每个数只能取一次,就是 01 背包。

  1. 确认 dp 数组及下标含义

dp [j]: 填满容积为 j 的背包,有 dp [j] 种方法。

  1. 确定递推公式

如果取当前的 nums [i],凑成 d [j] 就有 dp [j-nums [i]] 种方法。

递推公式为:$$dp [j] += dp [j - nums [i]]$$

  1. dp 数组如何初始化

dp [0] = 1,因为 dp [0] 是一切递推结果的开始。

dp [j] 其他下标对应的数值应该初始化为 0,从递推公式也可以看出,dp [j] 要保证是 0 的初始值,才能正确的由 dp [j - nums [i]] 推导出来。

  1. 确定遍历顺序

nums 为物品,left 为背包,先物品后背包。

  1. 举例推导 dp 数组

输入:nums: [1, 1, 1, 1, 1], S: 3

bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4

Alt text

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) sum += nums[i];
        if (abs(S) > sum) return 0; // 此时没有方案
        if ((S + sum) % 2 == 1) return 0; // 此时没有方案
        int bagSize = (S + sum) / 2;
        vector<int> dp(bagSize + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = bagSize; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[bagSize];
    }
};

如果能找到目标和的话 S + sum 不可能为奇数,因为数组元素都是整数,要找的子集和也应该是整数。如果为奇数,那题解中的 bagSize 就是小数,这不可能。

# 一零和

题目链接🔗

本题种 strs 数组里的元素就是物品,每个物品都是一个。m、n 相当于是一个背包,有两个维度。

  1. 确定 dp 数组以及下标的含义

dp [i][j]: 最多有 i 个 0 和 j 个 1 的 strs 的最大子集的大小为 dp [i][j]。

  1. 确定递推公式

dp [i][j] 可以由前一个 strs 里的字符串推导出来,strs 里的字符串有 zeroNum 个 0,oneNum 个 1。

dp [i][j] 就可以是 dp [i - zeroNum][j - oneNum] + 1。

所以递推公式:$$dp [i][j] = max (dp [i][j], dp [i - zeroNum][j - oneNum] + 1)$$

  1. dp 数组如何初始化

因为物品价值不会是负数,初始为 0,保证递推的时候 dp [i][j] 不会被初始值覆盖。

  1. 确定遍历顺序

物品就是 strs 里的字符串,背包容量就是题目描述中的 m 和 n。

  1. 举例推导 dp 数组

以输入:["10","0001","111001","1","0"],m = 3,n = 3 为例:

Alt text

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0)); // 默认初始化 0
        for (string str : strs) { // 遍历物品
            int oneNum = 0, zeroNum = 0;
            for (char c : str) {
                if (c == '0') zeroNum++;
                else oneNum++;
            }
            for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
};

# 完全背包理论基础

完全背包模板题:有 n 件物品和一个最多能背重量为 w 的背包。第 i 件物品的重量是 weight [i],得到的价值是 value [i] 。每件物品有无限多哥,求解将哪些物品装入背包里物品价值总和最大。

背包最大重量为 4,物品为:

重量价值
物品 0115
物品 1320
物品 2430

思路和 01 背包一致,但由于完全背包的物品是可以添加多次的,所以小从小到大遍历背包容量。遍历顺序先物品还是先背包都可以。

void CompletePack() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

# 零钱兑换 II

题目链接🔗

典型的完全背包问题。

  1. 确定 dp 数组以及下标的含义

dp [j]: 凑成总金额 j 的货币组合数为 dp [j]。

  1. 确定递推公式

dp [j] 是所有 dp [j - coins [i]] 相加。

所以递推公式为:$$dp [j] += dp [j-coins [i]]$$

  1. dp 数组如何初始化

首先 dp [0] 一定要为 1,dp [0] = 1 是递推公式的基础。dp [0]=1 还说明了一种情况:如果正好选了 coins [i] 后,也就是 j-coins [i] == 0 的情况表示这个硬币刚好能选,此时 dp [0] 为 1 表示只选 coins [i] 存在这样的一种选法。

下标非 0 的 dp [j] 初始化为 0,这样累计加 dp [j - coins [i]] 的时候才不会影响真正的 dp [j]

  1. 确定遍历顺序

本题要求的是硬币的组合数,不是排列数。for 循环的先后顺序有说法。

先遍历物品在遍历背包求出的事组合数,而先遍历背包再遍历物品求出的是排列数。

  1. 举例推导 dp 数组

输入: amount = 5, coins = [1, 2, 5] ,dp 状态图如下:

Alt text

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < coins.size(); i++) { // 遍历物品
            for (int j = coins[i]; j <= amount; j++) { // 遍历背包
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
};