从暴力递归到空间最优:完全背包问题的完整优化之路

一、问题理解:什么是完全背包?

先明确完全背包的核心定义,避免与 0-1 背包混淆:

给定一个容量为 V 的背包,n 个物品,每个物品有体积 volume[i] 和价值 value[i]每个物品可以无限次选取。要求在不超过背包容量的前提下,选择物品使总价值最大。

二、第一版代码:暴力递归(思路对但效率极低)

这是我最初的实现,完全贴合 “选 / 不选” 的递归逻辑,但存在诸多致命问题:

#include <iostream>
using namespace std;
int bag1(int value[], int volume[], int current_volume, int n) {
    if (current_volume == 0 || n == 0) {
        return 0;
    }
    if (current_volume < volume[n - 1]) {
        return bag1(value, volume, current_volume, n - 1);
    }
    if (current_volume >= volume[n - 1]) {
        return bag1(value, volume, current_volume - volume[n - 1], n) + value[n - 1] > bag1(value, volume, current_volume, n - 1) ? bag1(value, volume, current_volume - volume[n - 1], n) + value[n - 1] : bag1(value, volume, current_volume, n - 1);
    }

}
int main1()
{
    // 请在此输入您的代码
    int N, V;
    cin >> N >> V;
    int volume[10000], value[10000];
    for (int i = 0; i < N; i++) {
        cin >> volume[i] >> value[i];
    }
    cout << bag1(value, volume, V, N);


    return 0;
}

第一版代码的核心问题

  1. 重复计算子问题(效率灾难)递归过程中,大量相同的 “剩余容量 current_volume + 剩余物品数 n” 组合被反复计算。比如计算bag1(..., 5, 2)时,会多次调用bag1(..., 3, 2),时间复杂度远超 O(2^n)(因为允许重复选物品,递归分支更多),当n>20时几乎无法运行。
  2. 语法与规范性问题
    • 函数末尾无默认返回值,编译器会报警告;
    • int volume[10000], value[10000] 是 C99 变长数组特性,C++ 标准不支持,在 MSVC 等编译器中直接报错;
  3. 递归栈溢出风险递归深度随物品数和容量增加而剧增,当NV较大时,会触发栈溢出。
  4. 代码冗余三元运算符重复调用递归函数,同一子问题被计算多次(比如bag1(..., current_volume - volume[n-1], n)被调用了两次)。

三、第一轮优化:记忆化搜索(递归 + 备忘录)

针对 “重复计算子问题” 的核心痛点,我首先想到用记忆化搜索—— 新增一个 “备忘录” 数组,记录已计算过的子问题结果,避免重复递归。这是对暴力递归最直接的优化,且能保留递归的直观逻辑。

#include <iostream>
#include <vector>
#include <cstring> // 用于memset初始化
using namespace std;

// 备忘录:memo[n][v] 表示“前n个物品、剩余容量v”时的最大价值
int** memo;

// 封装max函数,避免冗余的三元运算符
int max(int a, int b) {
    return a > b ? a : b;
}

// 记忆化搜索版完全背包
int bag_memo(vector<int>& value, vector<int>& volume, int current_volume, int n) {
    // 基准条件
    if (current_volume == 0 || n == 0) {
        return 0;
    }

    // 核心优化:查备忘录,有结果直接返回
    if (memo[n][current_volume] != -1) {
        return memo[n][current_volume];
    }

    int res;
    // 当前物品体积超容量,只能不选
    if (current_volume < volume[n - 1]) {
        res = bag_memo(value, volume, current_volume, n - 1);
    } else {
        // 选当前物品(n不递减,可重复选) vs 不选当前物品
        int select = bag_memo(value, volume, current_volume - volume[n - 1], n) + value[n - 1];
        int not_select = bag_memo(value, volume, current_volume, n - 1);
        res = max(select, not_select);
    }

    // 存入备忘录,供后续复用
    memo[n][current_volume] = res;
    return res;
}

int main() {
    int N, V;
    cin >> N >> V;
    // 用vector替代变长数组,兼容所有C++编译器
    vector<int> volume(N), value(N);
    for (int i = 0; i < N; i++) {
        cin >> volume[i] >> value[i];
    }

    // 初始化备忘录:(n+1)行(物品数)、(V+1)列(容量)
    memo = new int*[N + 1];
    for (int i = 0; i <= N; i++) {
        memo[i] = new int[V + 1];
        // 初始值-1,表示该子问题未计算
        memset(memo[i], -1, (V + 1) * sizeof(int));
    }

    int max_value = bag_memo(value, volume, V, N);
    cout << max_value << endl;

    // 释放内存,避免内存泄漏
    for (int i = 0; i <= N; i++) {
        delete[] memo[i];
    }
    delete[] memo;

    return 0;
}

关键优化点详解

  1. 备忘录的核心作用memo[n][v] 缓存 “前 n 个物品、剩余容量 v” 的最大价值,每个子问题仅计算一次,时间复杂度从指数级骤降到 O(n∗V)(n为物品数,V为背包容量)。
  2. 修复代码冗余将 “选 / 不选” 的递归调用结果先保存到变量,再用max函数比较,避免同一子问题被重复调用。
  3. 规范性优化
    • vector替代变长数组,解决编译器兼容性问题;
    • 补充内存释放逻辑,符合 C++ 内存管理规范;
  4. 完全背包的递归特性选当前物品时,递归参数是n而非n-1—— 这是完全背包与 0-1 背包的核心区别,保证了物品可以被重复选取。

四、第二轮优化:迭代版二维 DP(消除递归栈)

记忆化搜索解决了效率问题,但递归仍有栈开销。我进一步将递归逻辑转化为迭代填充二维 DP 表,彻底摆脱递归,这也是理解完全背包状态转移的关键一步。

优化后代码(二维 DP)

#include <iostream>
using namespace std;
int max(int a, int b) {
    return a > b ? a : b;
}
int bag(int value[], int volume[], int total_volume, int n) {
    int max_value[n+1][total_volume+1];//max_value[i][j]表示容量为j时,前i个物品中挑选能选到的最大值
    for (int i = 0; i <= n; i++) {
        max_value[i][0] = 0;
    }
    for (int j = 0; j <= total_volume; j++) {
        max_value[0][j] = 0;
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= total_volume; j++) {
            if (volume[i - 1] > j) {
                max_value[i][j] = max_value[i - 1][j];
            }
            else {
                max_value[i][j] = max(max_value[i][j - volume[i - 1]] + value[i - 1], max_value[i - 1][j]);
            }
        }
    }
    return max_value[n][total_volume];
}
int main()
{
    // 请在此输入您的代码
    int N, V;
    cin >> N >> V;
    int volume[N], value[N];
    for (int i = 0; i < N; i++) {
        cin >> volume[i] >> value[i];
    }
    cout << bag(value, volume, V, N);


    return 0;
}

关键优化点详解

  1. 完全背包的状态转移方程(核心)这是与 0-1 背包最核心的区别,我用公式清晰总结:dp[i][j]={dp[i−1][j]max(dp[i−1][j],dp[i][j−volume[i−1]]+value[i−1])​若 volume[i−1]>j(物品体积超容量,不选)否则(选/不选取最大值)​选当前物品时,用dp[i][j-volume[i-1]]而非dp[i-1][j-volume[i-1]]—— 这意味着 “选了当前物品后,仍可以继续选该物品”,实现了 “无限次选取” 的逻辑。
  2. 消除递归栈迭代方式无函数调用开销,避免了栈溢出风险,执行效率更高;
  3. 状态定义更直观dp[i][j] 直接对应 “前 i 个物品、容量 j” 的状态,新手更容易理解 “状态” 和 “决策” 的关系。

五、终极优化:一维 DP(空间压缩到极致)

观察二维 DP 的状态转移方程可以发现:dp[i][j] 仅依赖 dp[i-1][j](上一行同列)和 dp[i][j-volume[i-1]](当前行左侧列)。因此,我可以将二维数组压缩为一维数组,空间复杂度从 O(n∗V) 降到 O(V)—— 这是完全背包的最优实现。

#include <iostream>
#include <vector>
using namespace std;

int max(int a, int b) {
    return a > b ? a : b;
}

// 一维DP版完全背包(空间最优,推荐)
int bag_dp_1d(vector<int>& value, vector<int>& volume, int total_volume, int n) {
    // 状态定义:dp[j] 表示“背包容量j”时的最大价值
    vector<int> dp(total_volume + 1, 0);

    // 遍历每个物品
    for (int i = 0; i < n; i++) {
        // 完全背包核心:容量正序遍历(允许重复选当前物品)
        for (int j = volume[i]; j <= total_volume; j++) {
            dp[j] = max(dp[j], dp[j - volume[i]] + value[i]);
        }
    }
    return dp[total_volume];
}

int main() {
    int N, V;
    cin >> N >> V;
    vector<int> volume(N), value(N);
    for (int i = 0; i < N; i++) {
        cin >> volume[i] >> value[i];
    }
    cout << bag_dp_1d(value, volume, V, N) << endl;
    return 0;
}

关键优化点详解

  1. 空间压缩原理一维数组dp[j]复用了二维数组的 “当前行” 数据,每次处理新物品时,从左到右(正序)更新,保证dp[j-volume[i]]已经包含了 “选当前物品多次” 的结果。
  2. 完全背包的一维核心:正序遍历容量这是与 0-1 背包最直观的区别:
    • 0-1 背包:容量逆序遍历(避免重复选同一物品);
    • 完全背包:容量正序遍历(允许重复选同一物品)。正序遍历会让dp[j]在更新时,优先使用已更新的dp[j-volume[i]](包含当前物品的选取结果),从而实现 “无限次选取”。
  3. 代码极简且高效一维 DP 是完全背包的工业级写法,代码行数最少、空间占用最小,也是面试和竞赛中最常考察的版本。

六、总结:完全背包优化的核心思想

从暴力递归到一维 DP,我的优化过程本质上是对 “动态规划核心思想” 的逐步落地,同时紧扣完全背包的独特性:

  1. 重叠子问题:用备忘录 / DP 表缓存重复计算的子问题,将指数级时间复杂度降到多项式级别;
  2. 最优子结构:“选 / 不选” 的决策依赖子问题的最优解,且完全背包通过 “选物品时不递减物品数(递归)/ 用当前行状态(二维 DP)/ 正序遍历容量(一维 DP)” 实现 “无限次选取”;
  3. 空间压缩:通过观察状态转移的依赖关系,移除冗余的物品维度,实现空间最优。

对于新手来说,建议按 “暴力递归→记忆化搜索→二维 DP→一维 DP” 的顺序学习:先理解递归的 “选 / 不选” 逻辑,再掌握二维 DP 的状态转移,最后吃透一维 DP 的正序遍历技巧 —— 这不仅能解决完全背包问题,也能为后续学习 “多重背包”“混合背包” 等扩展问题打下基础。

核心要点再强调:完全背包与 0-1 背包的唯一核心区别,就是 “选物品时是否允许重复选”—— 反映在代码上,递归是 n 是否递减、二维 DP 是用 i 还是 i-1、一维 DP 是容量正序还是逆序。抓住这一点,就能彻底区分两类背包问题。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇