跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
编程语言算法

动态规划专题:01 背包模型详解与空间优化

系统讲解 01 背包问题,涵盖状态定义、转移方程及二维到一维的空间优化。通过分割等和子集、目标和、最后一块石头重量 II 等经典例题,演示如何将实际问题转化为背包模型。提供 C++ 代码实现,解析逆序遍历原理,帮助读者掌握动态规划核心思想。

疯疯癫癫发布于 2026/3/28更新于 2026/7/2451 浏览
动态规划专题:01 背包模型详解与空间优化

一、前言:什么是背包问题?

核心本质:你有一个容量有限的背包,面前有一堆宝物,每个宝物都有体积和价值。你的目标是:在不撑破背包的前提下,让带走的宝物总价值最高。

01 的含义:每个物品只有一件。对于每个物品,你只有两个选择:要么选 (1),要么不选 (0)。这就是 01 背包的由来。

二、01 背包:标准模板与两种形态

2.1 题目描述

描述: 你有一个容量为 V 的背包,有 n 个物品。第 i 个物品体积为 v_i,价值为 w_i。 第一问:至多能装多大价值?(不一定装满) 第二问:恰好装满时,至多能装多大价值?(无解输出 0)

示例:3 件物品,容积 5。物品:(2, 10), (4, 5), (1, 4)。 第一问:14(选第 1 和第 3 件);第二问:9(选第 2 和第 3 件)。

2.2 深度拆解(第一问:不超过 V)

1. 状态表示

dp[i][j] 表示:从前 i 件物品中选,总体积不超过 j 的最大价值。

2. 状态转移方程

对于第 i 件物品(体积 v_i,价值 w_i),我们有两种选择:

  • 不选它:最大价值等于'前 i-1 件物品,体积不超过 j'时的价值。 dp[i][j] = dp[i-1][j]
  • 选它:前提是背包得装得下(j >= v_i)。最大价值等于'第 i 件的价值'加上'前 i-1 件物品,剩余空间 j - v_i'时的最大价值。 dp[i][j] = dp[i-1][j - v_i] + w_i

综合方程: dp[i][j] = max(dp[i-1][j], dp[i-1][j - v_i] + w_i)

3. 朴素二维代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int N = 1010;
int n, V;
int v[N], w[N];
int dp[N][N]; // 二维数组

int main() {
    cin >> n >> V;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];

    // 第一问:不超过 V
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= V; j++) {
            // 1. 不选第 i 个
            dp[i][j] = dp[i-1][j];
            // 2. 选第 i 个 (前提是装得下)
            if (j >= v[i]) dp[i][j] = max(dp[i][j], dp[i-1][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][V] << endl;

    // 第二问:恰好装满 V
    // 初始化:dp[0][0] = 0, 其余为 -INF (表示不可能)
    for (int j = 0; j <= V; j++) dp[0][j] = -0x3f3f3f3f;
    dp[0][0] = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= V; j++) {
            dp[i][j] = dp[i-1][j];
            if (j >= v[i] && dp[i-1][j - v[i]] != -0x3f3f3f3f) // 只有前一个状态合法才能转移
                dp[i][j] = max(dp[i][j], dp[i-1][j - v[i]] + w[i]);
        }
    }
    // 如果 dp[n][V] 还是负数,说明无解,输出 0
    cout << (dp[n][V] < 0 ? 0 : dp[n][V]) << endl;
    return 0;
}

2.3 进阶:空间优化(滚动数组)

1. 为什么要逆序?

观察方程:dp[i][j] 只依赖于上一行的 dp[i-1][...]。 如果我们去掉第一维,变成 dp[j]:

  • 正序遍历:计算 dp[j] 时,用到的 dp[j-v] 已经是本行更新过的值了(相当于物品 i 被选了多次)。
  • 逆序遍历:计算 dp[j] 时,j-v 在 j 的左边,还没被更新,依然是上一行的旧值。这正是我们想要的!
2. 优化后的一维代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int N = 1010;
int n, V;
int v[N], w[N];
int dp[N]; // 一维数组

int main() {
    cin >> n >> V;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];

    // 第一问:不超过 V
    for (int i = 1; i <= n; i++) {
        // 逆序遍历!从 V 到 v[i]
        for (int j = V; j >= v[i]; j--) {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[V] << endl;

    // 第二问:恰好装满 V
    // 初始化:只有容量为 0 时价值为 0,其余为 -INF
    for (int j = 1; j <= V; j++) dp[j] = -0x3f3f3f3f;
    dp[0] = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = V; j >= v[i]; j--) {
            if (dp[j - v[i]] != -0x3f3f3f3f) // 只有前置状态有效才更新
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << (dp[V] < 0 ? 0 : dp[V]) << endl;
    return 0;
}

三、分割等和子集:决策类背包

3.1 题目解析

描述:给你一个正整数数组,能否将其分割成两个子集,使它们的和相等?

示例:[1, 5, 11, 5] -> true (可以分成 {1, 5, 5} 和 {11})

3.2 思路转换

  • 转化:这道题等价于——能否从数组中选出一些数,让它们的和恰好等于 sum / 2。
  • 映射到背包:
    • 背包容量:sum / 2。
    • 物品:数组里的每个数(体积和价值都是数值本身)。
    • 目标:能否装满?

3.3 朴素二维代码

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for (int x : nums) sum += x;
        if (sum % 2 != 0) return false; // 奇数分不了
        int target = sum / 2;
        int n = nums.size();
        // dp[i][j] 表示前 i 个数能否凑成和 j
        vector<vector<bool>> dp(n + 1, vector<bool>(target + 1, false));
        
        // 初始化:凑成和为 0,不需要选任何数,天然为 true
        for (int i = 0; i <= n; i++) dp[i][0] = true;
        
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= target; j++) {
                // 不选第 i 个数
                dp[i][j] = dp[i-1][j];
                // 选第 i 个数
                if (j >= nums[i-1]) {
                    dp[i][j] = dp[i][j] || dp[i-1][j - nums[i-1]];
                }
            }
        }
        return dp[n][target];
    }
};

3.4 空间优化代码

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for (int x : nums) sum += x;
        if (sum % 2 != 0) return false;
        int target = sum / 2;
        vector<bool> dp(target + 1, false);
        dp[0] = true; // 初始化
        
        for (int x : nums) {
            // 逆序遍历,防止重复选择同一个数
            for (int j = target; j >= x; j--) {
                dp[j] = dp[j] || dp[j - x];
            }
        }
        return dp[target];
    }
};

四、目标和:求方案数

4.1 题目解析

描述:每个数字前加 + 或 -,凑成 target。问有多少种方法?

4.2 思路转换

设加正号的数总和为 P,加负号的数总和为 M。 P - M = target,且 P + M = sum。 两式相加得:2P = sum + target ⇒ P = (sum + target) / 2。 问题转化:从数组中选出一些数,让它们的和恰好等于 P,有多少种选法?

4.3 朴素二维代码

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int x : nums) sum += x;
        if (abs(target) > sum || (sum + target) % 2 != 0) return 0;
        int bagSize = (sum + target) / 2;
        int n = nums.size();
        // dp[i][j] 前 i 个数凑成和 j 的方案数
        vector<vector<int>> dp(n + 1, vector<int>(bagSize + 1, 0));
        dp[0][0] = 1; // 凑成 0 的方案数为 1 (空集)
        
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= bagSize; j++) {
                // 不选
                dp[i][j] = dp[i-1][j];
                // 选
                if (j >= nums[i-1]) {
                    dp[i][j] += dp[i-1][j - nums[i-1]];
                }
            }
        }
        return dp[n][bagSize];
    }
};

4.4 空间优化代码

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int x : nums) sum += x;
        if (abs(target) > sum || (sum + target) % 2 != 0) return 0;
        int bagSize = (sum + target) / 2;
        vector<int> dp(bagSize + 1, 0);
        dp[0] = 1;
        
        for (int x : nums) {
            // 逆序遍历
            for (int j = bagSize; j >= x; j--) {
                dp[j] += dp[j - x];
            }
        }
        return dp[bagSize];
    }
};

五、最后一块石头的重量 II:最小差值

5.1 题目解析

描述:每次选两块石头粉碎,剩下重量差。不断重复,求最后剩下的最小重量。

5.2 思路转换

我们要把石头分成两堆,让它们的重量尽可能接近 sum / 2。 这又是 01 背包:容量为 sum / 2,求能装入的最大重量。

5.3 朴素二维代码

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for (int x : stones) sum += x;
        int target = sum / 2;
        int n = stones.size();
        // dp[i][j] 前 i 个石头,容量 j,最大重量
        vector<vector<int>> dp(n + 1, vector<int>(target + 1, 0));
        
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= target; j++) {
                dp[i][j] = dp[i-1][j];
                if (j >= stones[i-1]) {
                    dp[i][j] = max(dp[i][j], dp[i-1][j - stones[i-1]] + stones[i-1]);
                }
            }
        }
        return sum - 2 * dp[n][target];
    }
};

5.4 空间优化代码

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for (int x : stones) sum += x;
        int target = sum / 2;
        vector<int> dp(target + 1, 0);
        
        for (int x : stones) {
            // 逆序遍历
            for (int j = target; j >= x; j--) {
                dp[j] = max(dp[j], dp[j - x] + x);
            }
        }
        return sum - 2 * dp[target];
    }
};

六、总结:01 背包的灵魂

核心对比卡片:

特性朴素二维 DP空间优化一维 DP
状态定义dp[i][j] (前 i 个,容量 j)dp[j] (容量 j)
空间复杂度O(N·V)O(V)
内层遍历顺序任意(通常正序)必须逆序 (V → v_i)
原因依赖 dp[i-1][...],行独立依赖旧值,防止同层覆盖

目录

  1. 一、前言:什么是背包问题?
  2. 二、01 背包:标准模板与两种形态
  3. 2.1 题目描述
  4. 2.2 深度拆解(第一问:不超过 V)
  5. 1. 状态表示
  6. 2. 状态转移方程
  7. 3. 朴素二维代码
  8. 2.3 进阶:空间优化(滚动数组)
  9. 1. 为什么要逆序?
  10. 2. 优化后的一维代码
  11. 三、分割等和子集:决策类背包
  12. 3.1 题目解析
  13. 3.2 思路转换
  14. 3.3 朴素二维代码
  15. 3.4 空间优化代码
  16. 四、目标和:求方案数
  17. 4.1 题目解析
  18. 4.2 思路转换
  19. 4.3 朴素二维代码
  20. 4.4 空间优化代码
  21. 五、最后一块石头的重量 II:最小差值
  22. 5.1 题目解析
  23. 5.2 思路转换
  24. 5.3 朴素二维代码
  25. 5.4 空间优化代码
  26. 六、总结:01 背包的灵魂
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Python 高阶函数:map() 原理、实战与常见陷阱
  • C++ STL set/map 模拟实现
  • 从 Alpaca 到 Vicuna:用 Llama Factory 切换对话模板
  • C++ 仿函数详解:对象像函数一样调用
  • SQL Join 实战:WHERE 与 ON 条件的性能差异解析
  • Java 并发编程基石:深入理解 synchronized 与 volatile 关键字
  • C++ 实现 2048 小游戏核心逻辑与代码解析
  • 大模型应用开发极简入门:GPT-4 原理与 LangChain 实战
  • C++ 实现 Java 风格 Stream API 的思路与代码
  • 城市场景下无人机三维路径规划的多目标粒子群优化算法 NMOPSO
  • Git 项目远程源迁移与本地初始化指南
  • Neo4j Desktop 2.0 安装教程:自定义安装路径
  • OpenRouter 详解:全球 AI 模型聚合平台与免费资源指南
  • Java Swing 文本输入框交互与事件监听实战
  • 深入理解 Linux 系统文件 I/O:从 open 到重定向的底层逻辑
  • VS Code 远程连接服务器后 GitHub Copilot 无法使用的解决方案
  • LLaMA Factory 大模型微调实战指南
  • Web技术核心与安全风险(三)Web 后端安全
  • AI 与大模型的核心差异深度解析
  • 基于 OpenClaw 与飞书集成构建 AI 新闻推送机器人

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online