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

动态规划:01 背包问题详解

讲解动态规划中的 01 背包问题,包括状态定义、递推公式及空间优化至一维数组的方法。通过回溯法对比引出动态规划优势,并提供了分割等和子集、最后一块石头的重量 II、目标和、一和零四个经典变体的 C++ 解题代码与思路分析。

leon发布于 2026/3/28更新于 2026/7/2048 浏览
动态规划:01 背包问题详解

01 背包

有 n 件物品,与一次最多能背 w 重量的背包。第 i 件物品,重量为 weight[i],得到的价值为 value[i]。

每件物品只能用一次,求解,将那些物品装入背包内,物品的价值总和最大。

重量 (weight)价值 (value)
物品 0115
物品 1320
物品 2430

这是一个标准的背包问题。很多一看到这个,就直接想起用动态规划,而忽略了暴力解法。

这是因为没有自下而上思考的结果。

如下代码,一般动态规划问题,都是能通过回溯解决,因为每个物品都有两种可能(状态),被放入背包,或者不放入背包。

// 全局变量用于记录最大价值
int maxValue = 0;
// 物品的重量和价值数组
vector<int> weights = {1, 3, 4, 5, 6};
vector<int> values = {1, 3, 4, 5, 6};
// 背包容量
int capacity = 10;
// 回溯函数
void backtrack(int index, int currentWeight, int currentValue) {
    // 如果已经遍历完所有物品
    if (index == weights.size()) {
        // 更新最大价值
        if (currentValue > maxValue) {
            maxValue = currentValue;
        }
        return;
    }
    // 不选择当前物品 - 01 背包中的 0
    backtrack(index + 1, currentWeight, currentValue);
    // 选择当前物品 - 01 背包中的 1
    if (currentWeight + weights[index] <= capacity) {
        backtrack(index + 1, currentWeight + weights[index], currentValue + values[index]);
    }
}

如上的回溯算法,每个问题都有两个解法,通过暴力解决,但通常这种解法,是 O(2^n) 的时间复杂度,随着数量的增加呈指数级上升。

而动态规划仅仅需要 O(N*M) 就可以解决。

第一步:下标含义

dp[i][j] 表示将前 i 件物品装进限重为 j 的背包可以获得的最大价值,0<=i<=N, 0<=j<=W

第二步:推导公式

那么我们可以将 dp[0][0...W] 初始化为 0,表示将前 0 个物品 (即没有物品),装入书包的最大价值为 0。那么当 i>0 时,dp[i][j] 有两种情况:

  1. 不装入第 i 件物品,即 dp[i-1][j];
  2. 装入第 i 件物品(前提是能装下),即 dp[i-1][j-weight[i]]+value[i]。

第三步:书写代码

dp[weight.size()][bagweight + 1]; // weight 数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
        if (j < weight[i]) 
            dp[i][j] = dp[i - 1][j];
        else 
            dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    }
}

空间压缩

递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])。

看到 dp[i][j] 与 dp[i-1][...] 的关系,可以发现,dp 都由上一行推导出来的 (也就是把 dp[i - 1] 那一层拷贝到 dp[i] 上),所以可以压缩代码。

把二维数组,压缩为一维滚动数组。

这也就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

需要注意的是,为了防止上一层循环的 dp[0,...,j-1] 被覆盖,循环的时候 j 只能逆向枚举。

for(int i = 0; i < weight.size(); ++i){
    for(int j = bagWeight; j>=weight[i]; j--){
        dp[j] = max( dp[j], dp[j-weight[i]]+value[i] );
    }
}

相关题目

1、分割等和子集

给你一个只包含正整数的非空数组 nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

**示例 1:**输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11]。

**示例 2:**输入:nums = [1,2,3,5] 输出:false 解释:数组不能分割成两个元素和相等的子集。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int cur = 0;
        for(int i:nums) cur+=i;
        int sum = cur/2;
        if(sum*2 != cur) return false; // 意外情况,直接排除
        vector<int> dp(sum+1,0);
        for(int i=0; i<nums.size(); i++){
            for(int j=sum; j>=nums[i]; --j){
                dp[j] = max(dp[j], dp[j-nums[i]]+nums[i]);
            }
        }
        return sum==dp[sum]?true:false;
    }
};

2、最后一块石头的重量 II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。 每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:如果 x == y,那么两块石头都会被完全粉碎;如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。 最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

**示例 1:**输入:stones = [2,7,4,1,8,1] 输出:1

**示例 2:**输入:stones = [31,26,33,21,40] 输出:5

核心思路是将石头分为两堆,使差值最小。

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for(int i : stones) sum+=i;
        int cur = sum;
        sum>>=1; // 右移 1 位,相当于除以 2
        vector<int> dp(sum+1, 0);
        for(int i=0; i<stones.size(); ++i){
            for(int j = sum; j>=stones[i]; --j){
                dp[j] = max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
        return cur-2*dp[sum];
    }
};

3、目标和

给你一个非负整数数组 nums 和一个整数 target。 向数组中的每个整数前添加 '+' 或 '-',然后串联起所有整数,可以构造一个表达式。 返回可以通过上述方法构造的、运算结果等于 target 的不同表达式的数目。

**示例 1:**输入:nums = [1,1,1,1,1], target = 3 输出:5

**示例 2:**输入:nums = [1], target = 1 输出:1

本题本质是数学推导问题。设正数和为 P,负数和为 N,则 P+N=sum, P-N=target,推导出 P=(sum+target)/2。只要找到和为 P 的子集数量即可。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int cur = 0;
        for(int i:nums) cur+=i;
        int sum = (cur-target)>>1;
        if(sum*2!=cur-target||sum<0) return 0; // 直接就没有可能了
        vector<int> dp(sum+1);
        dp[0]=1; // 公式推导出来的正整数
        for(int i=0; i<nums.size(); ++i){
            for(int j=sum; j>=nums[i]; --j){
                dp[j]=dp[j-nums[i]]+dp[j];
            }
        }
        return dp[sum];
    }
};

4、一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n。 请你找出并返回 strs 的最大子集的长度,该子集中最多有 m 个 0 和 n 个 1。

**示例 1:**输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出:4

**示例 2:**输入:strs = ["10", "0", "1"], m = 1, n = 1 输出:2

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m+1,vector<int>(n+1,0));
        for(string str : strs){
            int num0=0,num1=0;
            for(char c : str){
                if(c=='0') num0++;
                else num1++;
            }
            for(int i=m; i>=num0; --i){
                for(int j=n; j>=num1; --j){
                    dp[i][j] = max(dp[i][j],dp[i-num0][j-num1]+1);
                }
            }
        }
        return dp[m][n];
    }
};

目录

  1. 01 背包
  2. 第一步:下标含义
  3. 第二步:推导公式
  4. 第三步:书写代码
  5. 空间压缩
  6. 相关题目
  7. 1、分割等和子集
  8. 2、最后一块石头的重量 II
  9. 3、目标和
  10. 4、一和零
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 二叉树深度优先搜索算法与经典例题解析
  • 零基础网络安全入门学习指南
  • GitHub Copilot 与 Claude Code 功能对比与选型指南
  • AIGC 检测工具选择标准与论文降重修改策略
  • AI 大模型学习指南:从基础概念到实战应用
  • Java 入门:输入输出与方法
  • 网络安全防护体系建设
  • 基于 SpringBoot 的共享单车租借网络平台设计与实现
  • Flutter 三方库 bones_ui 鸿蒙化适配指南
  • JetBrains Copilot Agent Mode + MCP 配置实战
  • Maven 基础:在 IDEA 中构建与管理 Java 项目
  • 5 款免费 AIGC 检测工具推荐与降重实践指南
  • Flutter web_scraper 在鸿蒙端的适配与实战
  • C++ 继承入门:从概念定义到默认成员函数
  • Git Bash 本地版本控制基础操作指南
  • SpringBoot 手动开启数据库事务的几种实现方式
  • PaperZZ 论文查重与 AIGC 检测工具评测
  • RPA 技术实战指南:从原理到落地
  • Xilinx Vivado 付费 IP 核 License 状态解读与获取指南
  • Spring 中@Autowired 与@Resource 注解的核心区别

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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