2648 字
13 分钟
Day37-动态规划 part05

完全背包问题#

完全背包与0-1背包问题非常相似,不同之处在于:

每种物品可以选择无限次,而不是只能选一次。

假设有 N 件物品和一个容量为 W 的背包。每件物品有两个属性:

  • 体积:v[i]

  • 价值:w[i]

目标是从这些物品中选取若干件(每种物品可以选无限多次),使得在不超过背包容量的前提下,获得的最大总价值是多少。

完全背包的二维dp表示#

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

dp[i][j]:表示从下标为 [0-i] 的物品,每个物品可以取无限次,放进容量为 j 的背包,价值总和最大是多少。

2. 确定递推公式#

  • 不放物品 i:背包容量为 j,最大价值为 dp[i-1][j]
  • 放物品 i:背包空出物品 i 的体积 v[i],剩余容量为 j - v[i]dp[i][j - v[i]] 表示背包容量 为 j - v[i] 时的最大价值。那么放入物品 i 后的最大价值为:dp[i][j - v[i]] + w[i]
    • 放入物品 i 后,背包容量为 j - v[i],但是物品 i 还可以继续放入背包中,因此我们还是在考虑物品 i

因此递推公式为:

dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]);

这里与0-1背包的递推公式不同的是:dp[i][j - v[i]] 这里的 i 没有减去1,也就是说物品 i 还可以继续放入背包中。

0 - 1背包的递推公式为:

dp[i][j] = max(dp[i-1][j], dp[i-1][j - v[i]] + w[i]);

3. 初始化dp数组#

首先从 dp[i][j] 的定义出发,如果背包容量 j 为0的话,即 dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

初始化1

状态转移方程中: dp[i][j] = max(dp[i-1][j], dp[i-1][j - v[i]] + w[i]) 可以看出,dp[i][j] 依赖于 dp[i-1][j]dp[i][j - v[i]]。可以看出有一个方向 i 是由 i-1 推导出来,那么 i 为0的时候就一定要初始化。 因此我们需要初始化 dp[0][j],即当没有物品时,背包的最大价值为0。

dp[0][j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

  • 那么很明显当 j < v[0] 时,dp[0][j] 应该为0,因为背包容量不够放下物品0。

  • j >= v[0] 时,dp[0][j] 应该为 w[0],因为背包容量足够放下物品0。

j >= v[0]dp[0][j] 如果能放下 v[0] 的话,就一直装,每一种物品有无限个。

因此我们可以初始化 dp[0][j] 为:

vector<int> dp(W + 1, 0);
for (int j = 0; j <= W; ++j) {
    if (j >= v[0]) {
        dp[j] = w[0];
    }
}

4. 遍历顺序#

在0 - 1背包中,先遍历物品,再遍历背包容量,或者是先遍历背包容量,再遍历物品,都是可行的,因为这是由推导顺序决定的。

// 先遍历物品,再遍历背包容量
for (int i = 0; i < N; ++i) {
    for (int j = 1; j <= W; ++j) {
        if (j < v[i]) {
            dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
        } else {
            dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
        }
    }
}
// 先遍历背包容量,再遍历物品
for (int j = 0; j <= W; ++j) {
    for (int i = 1; i < N; ++i) {
        if (j < v[i]) {
            dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
        } else {
            dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
        }
    }
}

5. 整体代码#

vector<int> knapsack(int W, vector<int>& v, vector<int>& w) {
    int N = v.size();
    vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0)); // dp[i][j]表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少。
    
    for (int j = 0; j <= W; ++j) {
        if (j >= v[0]) {
            dp[0][j] = w[0];
        }
    }

    for (int i = 1; i < N; ++i) {
        for (int j = 0; j <= W; ++j) {
            if (j < v[i]) {
                dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
            } else {
                dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
            }
        }
    }

    return dp[N - 1]; // 返回最后一行,即所有物品的最大价值
}

完全背包的一维dp表示#

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

dp[j]:表示从下标为 [0-N] 的物品,每个物品可以取无限次,放进容量为 j 的背包,价值总和最大是多少。

2. 确定递推公式#

在上面的二维dp表示中,dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i])

for (int i = 0; i < N; ++i) {
    for (int j = 1; j <= W; ++j) {
        if (j < v[i]) {
            dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
        } else {
            dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
        }
    }
}

将上一层 dp[i-1] 的那一层拷贝到 当前层 dp[i] ,那么 递推公式由:dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]) 变成: dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])

所以我们可以将二维dp表示转化为一维dp表示。

dp[j] = max(dp[j], dp[j - v[i]] + w[i]);

  • 这里的 j 是从 Wv[i] 递减的,表示当前背包容量为 j,如果放入物品 i,那么剩余容量为 j - v[i]

3. 初始化dp数组#

vector<int> dp(W + 1, 0);

4. 遍历顺序#

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的,因为 dp[j] 是根据 下标 j 之前所对应的 dp[j] 计算出来的。 只要保证下标j之前的 dp[j] 都是经过计算的就可以了。

// 先遍历物品,再遍历背包容量
for (int i = 0; i < N; ++i) {
    for (int j = v[i]; j <= W; ++j) { // 从v[i]开始遍历
        dp[j] = max(dp[j], dp[j - v[i]] + w[i]); // 放下物品i
    }
}
// 先遍历背包容量,再遍历物品
for (int j = 0; j <= W; ++j) {
    for (int i = 0; i < N; ++i) { // 从0开始遍历
        if (j >= v[i]) {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]); // 放下物品i
        }
    }
}

零钱兑换 II#

518. 零钱兑换 II

1. 状态定义#

dp[i] 表示金额为 i 的组合数。

2. 状态转移方程#

dp[i] = dp[i] + dp[i - coins[j]]

意思是:对于当前硬币 coin,凑成金额 j 的方法数 = 凑成金额 j - coin 的方法数之和。

  • dp[i] 表示当前金额为 i 的组合数。
  • dp[i - coins[j]] 表示当前金额为 i - coins[j] 的组合数。

3. 初始化#

dp[0] = 1,表示凑成金额为0的组合数为1(即不选任何数字)。

4. 遍历顺序#

因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行。

而本题要求凑成总和的组合数,元素之间明确要求没有顺序。所以纯完全背包是能凑成总和就行,不用管怎么凑的。本题是求凑出来的方案个数,且每个方案个数是组合数。

正确的遍历顺序应该是:先遍历硬币,再遍历容量(金额)

for (int coin : coins) {
    for (int j = coin; j <= amount; ++j) {
        dp[j] += dp[j - coin];
    }
}

此时先遍历硬币,就是求组合。如果先遍历的是容量,那么就会出现重复计算的情况,也就是求的是排列。

5. 整体代码#

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1, 0);
        dp[0] = 1; // 凑出金额为0的组合数是1(空集)

        for (int coin : coins) {           // 遍历每种硬币(物品)
            for (int j = coin; j <= amount; ++j) { // 遍历容量,从小到大
                if (dp[j] < INT_MAX - dp[j - coin]) { //防止相加数据超int
                    dp[j] += dp[j - coin];
                }
            }
        }
        return dp[amount];
    }
};
  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。

  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品。

组合总和 IV#

377. 组合总和 IV

因为本题的描述中,元素相同但顺序不同的组合算作不同的组合,所以这其实是一个排列问题。

所以本题的思路和零钱兑换 II 是一样的,只不过本题是求排列数。

遍历顺序是:先遍历背包,再遍历物品。

这里就直接给出代码了:

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        // 求的是排列
        vector<unsigned int> dp(target + 1, 0);
        dp[0] = 1; // 凑出金额为0的组合数是1(空集)

        for (int i = 1; i <= target; ++i) { // 遍历容量,从小到大
            for (int num : nums) {           // 遍历每种硬币(物品)
                if (i >= num) {
                    dp[i] += dp[i - num];
                }
            }
        }
        return dp[target];
    }
};

爬楼梯 - 完全背包问题#

70. 爬楼梯

这里我们将爬楼梯的规则稍微改一下:

  • 每次可以爬 1 ~ 无限 步。
  • 也就是每次可以选择爬 1 步,2 步,3 步,…,n 步。

问题:有多少种不同的方式可以爬到楼梯的顶部?

那么这里就变成了一个完全背包问题了。

  • 跳多少阶楼梯 -> 背包容量
  • 爬楼梯的方式 -> 物品
背包问题爬楼梯
背包容量 n要达到的目标楼梯高度 n
物品每次可以选择的步数 1, 2, …, n
物品体积每次爬的步数
物品价值这里我们不是求最大价值,而是求组合数(方案数)

1. 状态定义#

dp[i] 表示爬到第 i 层楼梯的方式数。

2. 状态转移方程#

装满背包有几种方法,递推公式一般都是 dp[i] += dp[i - nums[j]];

本题呢,dp[i] 有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j]

那么递推公式为:dp[i] += dp[i - j]`

3. 初始化#

dp[0] = 1,表示爬到第0层楼梯的方式数为1(即不爬)。

4. 遍历顺序#

这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法是不同的,所以我们需要先遍历背包,再遍历物品。

for (int i = 1; i <= n; ++i) { // 遍历背包
    for (int j = 1; j <= m; ++j) { // 遍历物品
        if (i >= j) {
            dp[i] += dp[i - j];
        }
    }
}

5. 整体代码#

class Solution {
public:
    int climbStairs(int n) {
        vector<int> dp(n + 1, 0);
        dp[0] = 1; // 爬到第0层楼梯的方式数为1(即不爬)

        for (int i = 1; i <= n; ++i) { // 遍历背包
            for (int j = 1; j <= i; ++j) { // 遍历物品
                if (i >= j) {
                    dp[i] += dp[i - j];
                }
            }
        }
        return dp[n];
    }
};