Loading

13 0-1背包&完全背包

0-1背包问题

\(n\) 个物品,第 \(i\) 个物品的体积为 \(w[i]\),价值为 \(v[i]\)。每个物品至多选一个,求体积和不超过 \(capacity\) 的最大价值和。

回溯三问

  1. 当前操作? 枚举第 \(i\) 个物品选或不选。不选,剩余容量不变;选,剩余容量减少 \(w[i]\)
  2. 子问题? 在剩余容量为 \(c\) 时,从前 \(i\) 个物品中得到的最大价值和。
  3. 下一个子问题? 分类讨论:
    • 不选:在剩余容量为 \(c\) 时,从前 \(i-1\) 个物品中得到的最大价值和;
    • 选:在剩余容量为 \(c-w[i]\) 时,从前 \(i-1\) 个物品中得到的最大价值和。

综上可得:\(dfs(i, c) = max(dfs(i - 1, c), dfs(i - 1, c - w[i]) + v[i])\)

常见变形:

  1. 至多装 \(capacity\),求最大价值和;
  2. 恰好装 \(capacity\),求最大/最小价值和;
  3. 至少装 \(capacity\),求最小价值和

代码实现

点击查看代码
    int zero_one_knapsack(int capacity, vector<int>& w, vector<int>& v) {
        int n = w.size();
        auto dfs = [&](int i, int c) -> int {
            if (i < 0) {
                return 0;
            }
            if (c < w[i]) {
                return dfs(i - 1, c);
            }
            return max(dfs(i - 1, c), dfs(i - 1, c - w[i]) + v[i]);
        };
        return dfs(n - 1, capacity);
    }
  • 时间复杂度:\(O(2^n)\)
  • 空间复杂度:\(O(n)\)

1 目标和

image

1.1 解题思路

假设添加正号的和为 \(p\),那么添加负数的和为 \(s-p\)(\(s\) 为数组的和,所以 \(s-p\) 相当于负数和的绝对值),于是有 \(p-(s-p) = target\),进而可以得到: $$p = \frac{target + s}{2}$$

这样的话问题就变成从 \(nums\) 中选择一些数字,使其和 \(p\) 恰好等于 \((target + s)/2\)的方案数。

1.2 代码实现

点击查看代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int s = reduce(nums.begin(), nums.end(), 0);
        if (s + target < 0 || (s + target) % 2 != 0) {
            return 0;
        }
        int n = nums.size();
        auto dfs = [&](this auto&& dfs, int i, int sum) -> int {
            if (i < 0) {
                if (sum == 0) {
                    return 1;
                }
                return 0;
            }
            if (sum < nums[i]) {
                return dfs(i - 1, sum);
            }
            return dfs(i - 1, sum) + dfs(i - 1, sum - nums[i]);
        };
        return dfs(n - 1, (target + s) / 2);
    }
};
  • 时间复杂度:\(O(2^n)\)
  • 空间复杂度:\(O(n)\)

记忆化搜索可以优化时间复杂度到 \(O(n)\),那么能不能优化空间复杂度到 \(O(1)\) 呢?

改成递推。
已知 $$dfs(i, sum) = dfs(i - 1, sum) + dfs(i - 1, sum - nums[i])$$,进一步可以得到 $$ f[i][sum] = f[i - 1][sum] + f[i - 1][sum - nums[i]]$$,
所以有:

\[f[i + 1][sum] = f[i][sum] + f[i][sum - nums[i]]\]

以下是递推的做法。

点击查看代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int s = reduce(nums.begin(), nums.end(), 0);
        int t = s + target;
        if (t < 0 || t % 2 != 0) {
            return 0;
        }
        t = t / 2;
        int n = nums.size();
        // 初始化 $f$ 数组
        vector<vector<int>> f(n + 1, vector<int>(t + 1, 0));
        f[0][0] = 1;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j <= t; ++j) {
                if (j < nums[i]) {
                    f[i + 1][j] = f[i][j];
                } else {
                    f[i + 1][j] = f[i][j] + f[i][j - nums[i]];
                }
            }
        }
        return f[n][t];
    }
};
  • 时间复杂度:\(O(n*t)\)
  • 空间复杂度:\(O(n*t)\)

继续优化空间复杂度的思路:

  1. 滚动数组(两个数组)
    每次计算出 \(f[i-1]\) 后,后面就不会用到 \(f[i]\) 了。换句话说,每时每刻,只有两行数组在参与运算。比如把 \(f[1]\) 算完了,那么 \(f[0]\) 就用不到了,用 \(f[1]\) 推导 \(f[2]\),再将 \(f[2]\) 的结果覆盖 \(f[0]\)
点击查看代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int s = reduce(nums.begin(), nums.end(), 0);
        int t = s + target;
        if (t < 0 || t % 2 != 0) {
            return 0;
        }
        t = t / 2;
        int n = nums.size();
        // 滚动数组
        // 初始化 $f$ 数组
        vector<vector<int>> f(2, vector<int>(t + 1, 0));
        f[0][0] = 1;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j <= t; ++j) {
                if (j < nums[i]) {
                    f[(i + 1)%2][j] = f[i%2][j];
                } else {
                    f[(i + 1)%2][j] = f[i%2][j] + f[i%2][j - nums[i]];
                }
            }
        }
        return f[n%2][t];
    }
};
  • 时间复杂度:\(O(n*t)\)
  • 空间复杂度:\(O(t)\)
  1. 一个数组
    进一步可以发现,\(f[i + 1][j]\) 主要依赖于 \(f[i][j]\)\(f[i][j - nums[i]]\),所以可以考虑从后向前更新。
点击查看代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int s = reduce(nums.begin(), nums.end(), 0);
        int t = s + target;
        if (t < 0 || t % 2 != 0) {
            return 0;
        }
        t = t / 2;
        int n = nums.size();
        // 滚动数组
        // 初始化 $f$ 数组
        vector<int> f(t + 1, 0);
        f[0] = 1;
        for (int i = 0; i < n; ++i) {
            for (int j = t; j >= nums[i]; --j) {
                f[j] = f[j] + f[j - nums[i]];
            }
        }
        return f[t];
    }
};

完全背包

\(n\) 种物品, 第 \(i\) 种物品的体积为 \(w[i]\),价值为 \(v[i]\),每种物品无限次重复选,求体积和不超过 \(capacity\) 的最大价值和。

回溯三问:

  1. 当前操作? 枚举第 \(i\) 种物品选一个或不选;不选,剩余容量不变;选一个,剩余容量减少 \(w[i]\)
  2. 子问题? 在剩余容量为 \(c\) 时,从前 \(i\) 个物品中得到的最大价值和
  3. 下一个子问题? 分类讨论:
    • 不选:在剩余容量为 \(c\) 时,从前 \(i - 1\) 种物品中得到的最大价值和;
    • 选:在剩余容量为 \(c-w[i]\) 时,从前 \(i\) 🀄物品中得到的最大价值和。

综上,可以得到:$$dfs(i,c)=max(dfs(i-1,c), dfs(i, c-w[i])+v[i])$$

2 零钱兑换

image

2.1 代码实现

回溯:

点击查看代码
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        auto dfs = [&](this auto&&dfs, int i, int c) -> int {
            if (i < 0) {
                return c == 0 ? 0: INT_MAX / 2;
            } // 0表示合法,INT_MAX / 2 表示不合法
            if (coins[i] > c) {
                return dfs(i - 1, c);
            }
            return min(dfs(i - 1, c), dfs(i, c - coins[i]) + 1);
        };
        int res = dfs(n - 1, amount);
        return res < INT_MAX / 2 ? res: -1;
    }
};
- 时间复杂度:$O(n^n)$(假设 $amount = n$,而且 $coins = 1$) - 空间复杂度:$O(n)(递归调用栈)

记忆化搜索:

点击查看代码
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        vector<vector<int>> dp(n, vector<int>(amount + 1, -1));
        auto dfs = [&](this auto&&dfs, int i, int c) -> int {
            if (i < 0) {
                return c == 0 ? 0: INT_MAX / 2;
            } // 0表示合法,INT_MAX / 2 表示不合法
            auto& res = dp[i][c];
            if (res != -1) {
                return res;
            }
            if (coins[i] > c) {
                res = dfs(i - 1, c);
            } else {
                res = min(dfs(i - 1, c), dfs(i, c - coins[i]) + 1);
            }
            return res;
        };
        int res = dfs(n - 1, amount);
        return res < INT_MAX / 2 ? res: -1;
    }
};
  • 时间复杂度:\(O(n * amount)\)
  • 空间复杂度:\(O(n * amount)\)

改成递推

点击查看代码
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        // 初始化
        vector<vector<int>> f(n + 1, vector<int>(amount + 1, INT_MAX / 2));
        f[0][0] = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < amount + 1; ++j) {
                if (coins[i] > j) {
                    f[i+1][j] = f[i][j];
                } else {
                    f[i+1][j] = min(f[i][j], f[i+1][j-coins[i]] + 1);
                }
            }
        }
        int res = f[n][amount];
        return res < INT_MAX / 2 ? res: -1;
    }
};
  • 时间复杂度:\(O(n*amount)\)
  • 空间复杂度:\(O(n*amount)\)
点击查看代码
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        // 初始化
        vector<int> f(amount + 1, INT_MAX / 2);
        f[0] = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < amount + 1; ++j) {
                if (coins[i] > j) {
                    f[j] = f[j];
                } else {
                    f[j] = min(f[j], f[j-coins[i]] + 1);
                }
            }
        }
        int res = f[amount];
        return res < INT_MAX / 2 ? res: -1;
    }
};
  • 时间复杂度:\(O(n*amount)\)
  • 空间复杂度:\(O(amount)\)

0-1背包是倒序,完全背包是正序,那么怎么思考循环的顺序呢?

posted @ 2026-01-30 11:39  王仲康  阅读(5)  评论(0)    收藏  举报