AcWing
  • 首页
  • 课程
  • 题库
  • 更多
    • 竞赛
    • 题解
    • 分享
    • 问答
    • 应用
    • 校园
  • 关闭
    历史记录
    清除记录
    猜你想搜
    AcWing热点
  • App
  • 登录/注册

AcWing 2. 01背包问题    原题链接    简单

作者: 作者的头像   well188 ,  2025-05-09 22:39:29 · 河北 ,  所有人可见 ,  阅读 2


0


朴素解法

关于01背包问题及其动态规划解法的讲解(特别是针对C++代码)重新梳理成题解。

01背包问题

你拥有:
N 件物品。
一个容量为 V 的背包。
每件物品 i 有一个体积 v_i 和一个价值 w_i。
约束:每件物品只能使用一次(即,要么选择放入背包,要么不放)。

目标:
选择物品的一个子集放入背包。
所选物品的总体积不得超过背包容量 V。
所选物品的总价值应尽可能大。
输出这个可能的最大总价值。

C++ 代码分析

我们来看一下你代码中的变量和结构:
n:物品的数量(从输入读取,对应问题描述中的 N)。
m:背包的容量(从输入读取,对应问题描述中的 V)。
v[i]:第 i 件物品的体积(1索引,即 v[1] 到 v[n])。
w[i]:第 i 件物品的价值(1索引,即 w[1] 到 w[n])。
* f[i][j]:动态规划(DP)表。这是解决方案的核心。

动态规划方法 - 步骤详解

  1. 识别最优子结构:
    该问题具有最优子结构。如果我们想求解 n 件物品在容量为 m 的背包中的最大价值,我们可以考虑第 n 件物品:

    • 情况1:我们不放入第 n 件物品。 问题就简化为在前 n-1 件物品中,用同样的容量 m 来获取最大价值。
    • 情况2:我们放入第 n 件物品(前提是其体积 v[n] 小于或等于当前容量 m)。 获得的价值将是 w[n] 加上在前 n-1 件物品中,用剩余容量 m - v[n] 所能获得的最大价值。
      最优解将是这两种情况中价值较大的那个。这个逻辑递归地适用于任何物品 i 和容量 j。
  2. 定义状态(匹配你代码中的 f[i][j]):
    在你代码中,状态 f[i][j] 代表:
    从前 i 件物品中(即物品 1, 2, …, i)进行选择,当背包容量为 j 时,所能获得的最大总价值。

    • i 的范围是从 0 到 n(已考虑的物品数量)。
    • j 的范围是从 0 到 m(当前正在考虑的背包容量)。
  3. 推导状态转移方程(你代码循环中的核心逻辑):
    对于每件物品 i(从1到 n),以及对于每种可能的容量 j(从0到 m):

    我们需要决定是否放入第 i 件物品(其体积为 v[i],价值为 w[i])。

    • 选项1:不放入第 i 件物品。
      如果我们不放入第 i 件物品,那么在容量为 j 的情况下我们能得到的最大价值,就等于只使用前 i-1 件物品在同样容量 j 下所能得到的最大价值。
      所以,价值为 f[i-1][j]。
      这对应代码行:f[i][j] = f[i-1][j];

    • 选项2:放入第 i 件物品。
      这只有在当前容量 j 大于或等于第 i 件物品的体积时才可能(即 j >= v[i])。
      如果我们放入第 i 件物品,它将贡献 w[i] 的价值。背包的剩余容量变为 j - v[i]。然后我们需要用前 i-1 件物品来最优地填充这个剩余容量。该子问题的最大价值是 f[i-1][j - v[i]]。
      所以,如果我们放入第 i 件物品,价值将是 f[i-1][j - v[i]] + w[i]。

    合并选项: 我们想要最大化价值。所以 f[i][j] 将是这两个选项中的最大值(如果选项2有效)。
    这正是你的代码所做的:
    c++ f[i][j] = f[i-1][j]; // 选项1:假设我们不拿第 i 件物品 if (j >= v[i]) { // 检查选项2是否可行 // 选项2:拿第 i 件物品,并与选项1的价值比较 f[i][j] = max(f[i][j], f[i-1][j - v[i]] + w[i]); }

  4. 确定边界条件/初始状态:

    • f[0][j] = 0 对于所有 j 从 0 到 m:如果我们不考虑任何物品(即考虑0件物品),无论背包容量如何,能获得的最大价值都是0。
    • f[i][0] = 0 对于所有 i 从 0 到 n:如果背包容量为0,我们无法放入任何物品,所以最大价值是0。

    在你的C代码中,f 是一个全局数组。在C中,如果全局数组没有显式初始化,它们会被自动零初始化。这有效地将所有的 f[0][j] 和 f[i][0](当 i=0 或 j=0 时)设置为0,从而满足了边界条件。
    当 i = 1 时,f[i-1][j] 变成 f[0][j],其值为0。
    当 j = 0 时,f[i][0] 将是 f[i-1][0](其值为0)。条件 if (0 >= v[i]) 通常为假(假设物品体积 v[i] 是正数),所以 f[i][0] 保持为 f[i-1][0]。

  5. 确定计算顺序(你代码中的循环):
    你代码中的循环定义了 f 表是如何被填充的:
    c++ for (int i = 1; i <= n; i++) // 外层循环:遍历物品 for (int j = 0; j <= m; j++) // 内层循环:遍历容量 { // ... f[i][j] 的计算 ... }

    • 外层循环 i 从 1 遍历到 n(逐个考虑物品)。
    • 内层循环 j 从 0 遍历到 m(考虑当前物品集合下的所有可能容量)。

    这个顺序至关重要。在计算 f[i][j] 时,它依赖于前一行 f[i-1][...] 的值(即少考虑一件物品的子问题的解)。这种“自底向上”的方法确保了在计算 f[i][j] 时,f[i-1][j] 和 f[i-1][j - v[i]] 都已经被计算并存储好了。

  6. 最终答案:
    循环完成后,f 表就被填充完毕。根据我们对状态的定义,f[n][m] 将存储:
    “从前 n 件物品中(即所有物品)进行选择,当总体积不超过 m(总背包容量)时,所能获得的最大价值。”
    这正是原问题的解。
    你的代码通过以下语句输出这个结果:cout << f[n][m] << endl;

01背包DP思考过程总结:

  1. 我要优化什么? (总价值)
  2. 对于每件物品我有哪些选择? (拿,或者不拿)
  3. 我如何定义一个状态 dp[i][j](或 f[i][j])来代表一个子问题的解?
    • 通常涉及 i(到目前为止考虑的物品数量)和 j(当前的容量/约束)。
    • f[i][j] = 使用前 i 件物品,在容量为 j 的情况下的最大价值。
  4. 我如何从较小的子问题过渡到较大的子问题? (递推关系式)
    • f[i][j] 基于 f[i-1][...]。
    • 考虑不拿第 i 件物品:f[i-1][j]。
    • 考虑拿第 i 件物品:第 i 件物品的价值 + f[i-1][j - 第 i 件物品的体积]。
    • f[i][j] = max (这两个选项)。
  5. 最简单的情况(边界条件)是什么?
    • f[0][j] = 0(没有物品,没有价值)。
    • f[i][0] = 0(没有容量,没有价值)。
  6. 我应该按什么顺序填充我的DP表?
    • 先遍历物品,再遍历容量。

这种系统性的方法,与你的C++代码相结合,为使用动态规划解决01背包问题提供了一条清晰的路径。

空间优化解法

好的,我们来讨论如何对上面讲解的01背包问题进行空间优化。

原来的二维DP解法中,我们使用了 f[i][j] 这样一个二维数组(或 vector<vector<int>>),其空间复杂度是 O(N*V)(其中 N 是物品数量,V 是背包容量)。当 N 和 V 很大时,这可能会导致内存超限。

空间优化的思路

观察状态转移方程:
f[i][j] = f[i-1][j] (不选第 i 件物品)
f[i][j] = max(f[i][j], f[i-1][j - v[i]] + w[i]) (如果 j >= v[i],考虑选第 i 件物品)

我们发现,在计算第 i 行(即 f[i][...])的状态时,我们仅仅依赖于第 i-1 行(即 f[i-1][...])的状态。我们不需要 f[i-2], f[i-3] 等更早的状态。

这就启发我们,其实没有必要存储整个二维 f 表。理论上,我们只需要两个一维数组:一个存储第 i-1 行的状态,另一个用来计算并存储第 i 行的状态。甚至,我们可以优化到只用一个一维数组。

使用一维数组进行优化

我们可以定义一个一维数组 dp[j](或者在你的代码中,可以继续叫 f[j],只是它现在是一维的),其中 dp[j] 表示:
在处理到当前物品为止,对于容量为 j 的背包,所能获得的最大价值。

当我们考虑第 i 件物品(体积 v[i],价值 w[i])时,我们想要更新 dp[j] 的值。
状态转移方程可以改写成(尝试):
dp[j] = max(dp[j], dp[j - v[i]] + w[i])

这里有一个关键问题:当我们计算 dp[j] 时,dp[j - v[i]] 应该代表什么?
它应该代表 在不考虑第 i 件物品时,容量为 j - v[i] 的背包所能获得的最大价值(即对应二维表中的 f[i-1][j - v[i]])。
而 dp[j] 在等号左边,代表的是 考虑了第 i 件物品后,容量为 j 的背包所能获得的最大价值(即对应 f[i][j])。
* dp[j] 在等号右边的 max 函数的第一个参数,也应该代表 不考虑第 i 件物品时,容量为 j 的背包所能获得的最大价值(即对应 f[i-1][j])。

关键点:内层循环(容量 j)的遍历顺序

如果我们在内层循环中,像二维DP那样正序遍历容量 j (从 0 到 m):
for (int j = v[i]; j <= m; j++) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
当计算 dp[j] 时,我们用到的 dp[j - v[i]] 可能已经是本轮(即考虑第 i 件物品时)更新过的值。例如,计算 dp[5] 时用到 dp[2],如果 dp[2] 已经在考虑第 i 件物品时被更新了,那么就相当于第 i 件物品在计算 dp[5] 时被间接考虑了多次(因为它影响了新的 dp[2],而新的 dp[2] 又用来计算 dp[5])。这就变成了完全背包问题(每件物品可以选多次)。

为了确保 dp[j - v[i]] 是上一轮(即处理第 i-1 件物品时)的状态值,我们必须逆序遍历背包容量 j(从 m 向下到 v[i])。
for (int j = m; j >= v[i]; j--) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
这样,当我们计算 dp[j] 时:
dp[j] (在 max 的第一个参数) 存储的是不放入物品 i 时,容量 j 的最大价值 (这是上一个物品 i-1 迭代结束时的 dp[j] 值,或者是本轮 i 更大容量 j' 更新前的值,但对于当前的 j,它代表了不放物品 i 的情况)。
dp[j - v[i]] 存储的是不放入物品 i 时,容量 j - v[i] 的最大价值。因为 j - v[i] 比 j 小,所以在逆序遍历中,dp[j - v[i]] 尚未被当前物品 i 更新,它仍然是上一轮迭代(处理物品 i-1)的结果。

这样,空间复杂度就从 O(N*V) 降到了 O(V)。

具体的C++代码 (空间优化后)

使用与你之前代码相似的变量名:

#include <iostream>
#include <algorithm> // 为了 std::max
#include <vector>    // 使用 vector 更灵活,但也可以用静态数组

using namespace std;

// 如果使用静态数组,可以像之前一样定义常量
// const int MAX_N = 1010; // 最大物品数量
// const int MAX_M = 1010; // 最大背包容量

int main() {
    ios_base::sync_with_stdio(false); // 加速cin/cout
    cin.tie(NULL);

    int n, m; // n: 物品数量, m: 背包容量
    cin >> n >> m;

    // 使用 vector 存储物品的体积和价值
    // v_item 和 w_item 下标从 0 开始,对应物品 0 到 n-1
    // 或者,如果你习惯 1-based 索引,可以声明 vector 大小为 n+1
    vector<int> v(n); // 体积
    vector<int> w(n); // 价值

    for (int i = 0; i < n; i++) {
        cin >> v[i] >> w[i];
    }

    // dp[j] 表示容量为 j 的背包能获得的最大价值
    // 对应原二维 f[i][j] 中的滚动数组思想
    vector<int> dp(m + 1, 0); // 初始化所有元素为0,容量从0到m

    // 遍历每一件物品
    for (int i = 0; i < n; i++) { // 物品索引从 0 到 n-1
        // 逆序遍历背包容量
        // 确保每个物品只被考虑一次(01背包特性)
        // j 必须大于等于当前物品体积 v[i] 才能考虑放入
        for (int j = m; j >= v[i]; j--) {
            // 决策:
            // 1. 不放入第 i 件物品:dp[j] 的值保持不变(来自上一个物品迭代的结果)
            // 2. 放入第 i 件物品:价值为 w[i] + dp[j - v[i]]
            //    (dp[j - v[i]] 是上一个物品迭代中,容量为 j - v[i] 的最大价值)
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }

    cout << dp[m] << endl; // 最终答案是容量为 m 时的最大价值

    return 0;
}

代码说明与对应关系:

  • 原 f[i][j]:现在由一维数组 dp[j] 滚动更新。
  • 物品循环 for (int i = 1; i <= n; i++):对应新代码中的 for (int i = 0; i < n; i++)。这里的 v[i] 和 w[i] 是当前正在考虑的物品。
  • 容量循环 for (int j = 0; j <= m; j++) 中的 f[i-1][j]:这部分信息隐含在 dp[j] 更新前的值中。
  • 容量循环 for (int j = 0; j <= m; j++) 中的 f[i-1][j - v[i]]:这部分信息通过逆序遍历,在 dp[j - v[i]] 中获取,确保它是上一个物品迭代的结果。
  • 核心状态转移 if (j >= v[i]) f[i][j] = max(f[i-1][j], f[i-1][j - v[i]] + w[i]); else f[i][j] = f[i-1][j];:
    在优化后的代码中,dp[j] = max(dp[j], dp[j - v[i]] + w[i]); 实现了这个逻辑。
    • dp[j] (max的第一个参数) 代表不选当前物品 i 时的价值,即 f[i-1][j]。
    • dp[j - v[i]] + w[i] 代表选当前物品 i 时的价值,其中 dp[j - v[i]] 来自 f[i-1][j - v[i]]。
    • 如果 j < v[i],则循环 for (int j = m; j >= v[i]; j--) 不会执行到这个 j,dp[j] 的值保持不变,天然对应了 f[i][j] = f[i-1][j] 的情况。

这种空间优化是01背包问题的一个标准技巧,非常实用。

0 评论

App 内打开
你确定删除吗?
1024
x

© 2018-2025 AcWing 版权所有  |  京ICP备2021015969号-2
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标 qq图标
请输入绑定的邮箱地址
请输入注册信息