dp背包总结

动态规划背包问题详解:0-1背包与完全背包

引言

背包问题是动态规划中的经典问题,也是面试中高频考察的算法模型。根据物品是否可以被重复选取,背包问题主要分为0-1背包完全背包。理解这两种背包问题的原理、递推公式、初始化以及遍历顺序,是解决一系列相关题目(如分割等和子集、最后一块石头的重量、目标和、零钱兑换等)的基础。

本文将从理论基础出发,详细讲解0-1背包和完全背包的解法,并对比两者的核心区别。


一、0-1背包问题

1.1 问题描述

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],价值是value[i]。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

例如:

  • 背包最大重量:4
  • 物品:
    • 物品0:重量1,价值15
    • 物品1:重量3,价值20
    • 物品2:重量4,价值30

问背包能背的最大价值是多少?

1.2 二维dp数组解法

1.2.1 确定dp数组含义

dp[i][j] 表示从下标为[0..i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

1.2.2 递推公式

对于物品i,有两种情况:

  • 不放物品idp[i][j] = dp[i-1][j]
  • 放物品idp[i][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])

1.2.3 初始化

  • dp[i][0] = 0(背包容量为0,价值为0)
  • 对于第一行i=0
    • j < weight[0]时,dp[0][j] = 0
    • j >= weight[0]时,dp[0][j] = value[0]

其他位置可以初始化为0(因为后续会被覆盖)。

1.2.4 遍历顺序

外层循环遍历物品,内层循环遍历背包容量。先遍历物品还是先遍历背包都可以,但通常先遍历物品更好理解。注意内层循环从小到大遍历。

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]);
    }
}

1.2.5 举例推导

以上述例子,最终dp[2][4] = 35

1.3 一维dp数组(滚动数组)解法

为了优化空间,可以将二维数组压缩为一维数组。因为dp[i][j]只依赖于上一行的数据,我们可以只用一个一维数组,在遍历时从后向前更新,确保每个物品只被放入一次。

1.3.1 确定dp数组含义

dp[j] 表示容量为j的背包,所背物品的最大价值。

1.3.2 递推公式

dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

1.3.3 初始化

dp[0] = 0,其他非0下标也初始化为0(因为价值都是正数)。

1.3.4 遍历顺序

外层遍历物品,内层遍历背包容量且必须倒序。倒序是为了保证每个物品只被放入一次(如果正序,同一物品可能被多次放入)。

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.3.5 举例

用物品0、1、2依次更新,最终dp[4] = 35


二、完全背包问题

2.1 问题描述

与0-1背包唯一不同:每件物品都有无限个(可重复放入)。背包容量为W,物品重量和价值同上,求最大总价值。

2.2 一维dp数组解法

完全背包也可以使用一维dp数组,但遍历顺序需要改变。

2.2.1 递推公式

与0-1背包一样:

dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

2.2.2 初始化

同样,dp[0] = 0,其他为0。

2.2.3 遍历顺序

外层遍历物品,内层遍历背包容量且必须正序。因为物品可以重复添加,所以需要从小到大的顺序,使得同一物品可以多次累加。

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量,正序
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

2.3 关于两层for循环的先后顺序

对于纯完全背包问题(即求最大价值),先遍历物品再遍历背包先遍历背包再遍历物品都是可以的。因为无论哪种顺序,dp[j]的计算都依赖于之前的状态,且最终结果相同。

例如先遍历背包再遍历物品:

for(int j = 0; j <= bagWeight; j++) { // 遍历背包
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        if (j - weight[i] >= 0)
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

但需要注意:当题目要求求组合数或排列数时,遍历顺序就至关重要

  • 求组合数(不讲究顺序):外层遍历物品,内层遍历背包(如零钱兑换II)
  • 求排列数(讲究顺序):外层遍历背包,内层遍历物品(如组合总和IV)

这一点在解决具体题目时需要仔细区分。


三、0-1背包与完全背包的对比

特性 0-1背包 完全背包
物品数量 每个物品只能用一次 每个物品可以用无限次
一维dp遍历顺序 内层循环倒序 内层循环正序
两层for循环顺序 只能先遍历物品,再遍历背包(一维) 先物品后背包或先背包后物品均可(纯最大价值)
典型应用 分割等和子集、最后一块石头的重量II 零钱兑换、完全平方数、单词拆分

四、总结

背包问题的核心在于递推公式遍历顺序的理解。对于0-1背包,一维数组必须倒序遍历以保证每个物品只取一次;对于完全背包,正序遍历允许重复选取。此外,当问题涉及“装满背包有多少种方式”时,还需要根据组合或排列要求调整两层循环的先后顺序。

掌握了这两种基础背包模型,就能解决LeetCode上大量的动态规划题目。建议读者在学习过程中,多动手推导dp数组,理解每一步的状态转移,这样才能真正融会贯通。


希望这篇博客能帮助你彻底理清0-1背包和完全背包的精髓!如有疑问,欢迎留言讨论。

posted @ 2026-03-07 15:48  Leon_LL  阅读(0)  评论(0)    收藏  举报