跳转至

背包问题*

百度百科:

背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?

利用背包思想对各种硬币找零问题求解。

01 背包问题*

给定不同面额的硬币 coins 和总金额 amount,每个硬币最多选择一次。计算可以凑成总金额所需的最少硬币个数,无法组合则返回 -1。

状态表示*

dp[i][j] 表示对于前 i 个硬币,总价值是 j 的情况下最小硬币数目。

状态转移*

dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - coins[i]] + 1) 分别对应了不拿和拿第 i 个硬币的情况。

因为 dp[i][j] 只与上一层的两个状态优先,因此可以优化为一维数组。dp[j] = min(dp[j], dp[j - coins[i]] + 1); 因为 j - coins[i] < j,所以需要倒序遍历,以保证上一状态不被覆盖。

边界情况*

dp[0] = 0 表示凑出金额 0 的最小硬币个数为 0。状态初始化为 amount + 1 表示无法达到。

int knapspack(vector<int>& coins, int amount) {
    vector<int> dp(amount + 1, amount + 1);
    dp[0] = 0;
    for(auto coin : coins) {
        for(int j = amount; j >= coin; j--) {
            dp[j] = min(dp[j], dp[j - coin] + 1);
        }
    }
    return dp[amount] == amount + 1 ? -1: dp[amount];
}

完全背包问题*

给定不同面额的硬币 coins 和总金额 amount。每个硬币可以选择无数次。计算可以凑成总金额所需的最少的硬币个数,无法组合则返回 -1。

状态表示*

d[i][j] 表示对前 i 种硬币,凑出金额 j 的最少硬币数目。

状态转移*

对第 i 种硬币,可以不拿,或拿 1...k 个,直到大于目标金额。

dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - coins[i]] + 1, dp[i - 1][j - 2 * coins[i]] + 2,..., f[i - 1][j - k * coins[i]] + k)

其中也包含了很多的冗余计算,如 dp[i][j - coins[i]] = min(dp[i - 1][j - coins[i]], dp[i - 1][j - 2 * coins[i]] + 1, ...)

可以合并为 dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i]] + 1)

优化为一维数组 dp[j] = min(dp[j], dp[j - coins[i] + 1]),从小到大枚举金额,j - coins[i] 的状态在同一层计算好直接替换即可。

边界情况*

dp[0] = 0,其余初始化为 amount + 1 表示无法达到。

int knapspacl(vector<int>& coins, int amount) {
    vector<int> dp(amount + 1, amount + 1);
    dp[0] = 0;
    for(auto coin : coins) {
        for(int j = coin; j <= amount; j++) {
            dp[j] = min(dp[j], dp[j - coin] + 1);
        }
    }
    return dp[amount] == amount + 1 ? -1 : dp[amount];
}

通过代码可以看到,完全背包的硬币问题与 01 背包下的问题几乎相同,就是内层循环金额**从小到大**遍历。

多重背包问题*

给定不同面额的硬币 coins 和总金额 amount。每个硬币选择的次数有限制为 s。计算可以凑成总金额的最少硬币个数,无法组合则返回 -1。

状态表示*

对第 i 种硬币,可以不拿,或拿 1...k 个,知道大于目标金额或达到数量限制。

dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - coins[i]] + 1,..., dp[i - 1][j - k * coins[i]] + k)

本质上与 01 背包类似,加一个枚举硬币个数的循环即可。

int knapspack(vector<int>& coins, int amount, vector<int>& s) {
    vector<int> dp(amount + 1, amount + 1);
    dp[0] = 0;
    for(int i = 0; i < coins.size(); i++) {
        for(int j = amount; j >= coins[i]; j--) {
            for(int k = 1; k <= s[i]; k++){
                dp[j] = min(dp[j], dp[j - k * coins[i]] + k);
            }
        }
    }
    return dp[amount] == amount + 1 ? -1: dp[amount];
}

最后更新: July 23, 2022