news 2026/9/15 7:57:14

高频必考!0-1背包:分割等和子集,一维优化为什么必须倒序?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
高频必考!0-1背包:分割等和子集,一维优化为什么必须倒序?

用爬楼梯立起了DP五步曲。今天进入DP面试考查率最高的家族——背包问题

LC.416「分割等和子集」是0-1背包的经典入门题,但它的杀伤力远超一道中等题:面试官会盯着你的代码问一句——

“一维优化时,容量为什么必须倒序遍历?正序到底会出什么事?”

这个问题答不上来,说明你的一维背包只是背下来的;答得上来,面试官就知道你真懂 DP


📦 题目速览(30秒读懂)

给你一个只含正整数的非空数组nums,判断能否分割成两个元素和相等的子集。

示例:[1,5,11,5]true(分成[1,5,5][11]
示例:[2,2,3,5]false

约束:n ≤ 200,nums[i] ≤ 100。

前置剪枝

sum为奇数,直接返回false——两相等整数之和必为偶数。

若为偶数,问题变成:能否从数组中选若干个数,恰好凑出target = sum/2


🧠 核心思路:识别背包 → 二维打底 → 一维提速

暴力为什么不行?

每个数字有“选/不选”两种命运,n=200时组合数2^200,暴力枚举子集等于自杀。

识别背包:这是0-1背包的“存在性”问题

  • 每个数字要么选、要么不选(对单个子集而言)→ 0-1背包指纹
  • 数字的值 = 重量 = 价值
  • 背包容量 = target
  • 问的是“能否恰好装满”→ 存在性问题

状态定义(二维)

dp[i][j]= 从前 i 个数字中能否选出若干个,和恰好为 j(布尔值)。

状态转移方程

考虑第 i 个数字num

  • 不选它dp[i-1][j]
  • 选它(前提j >= num):dp[i-1][j-num]
dp[i][j] = dp[i-1][j] || (j >= num && dp[i-1][j-num])

初始化

  • dp[0][0] = true(0个数字凑出0,天然成立)
  • dp[0][j>0] = false

一维优化:核心是“倒序”

观察转移方程,第 i 行只依赖第i-1行——二维表可以压成一行dp[j]

但压行会引发一个致命问题:覆盖。

二维里dp[i][j]依赖的是上一行dp[i-1][j-num]。压成一行后:

  • 正序(错误):算dp[j]dp[j-num]已被本轮更新成第i行的值——相当于“同一个数字被选了两次”,悄悄变成完全背包。
  • 倒序(正确):先算大j,它依赖的小j-num还没被动过,依然是“上一行”的旧值。

一句话记住:倒序保证dp[j-num]永远是上一行的旧值,每个物品恰好决策一次


🖼️ 图解算法(手把手走一遍)

nums = [1, 5, 11, 5]sum = 22target = 11。二维 DP 表(✓ = true):

dp[i][j]j=0j=1j=2j=5j=10j=11
i=0(无数字)
i=1(+1)
i=2(+5)
i=3(+11)
i=4(+5)

关键在第 4 行 j=11:选 11 →dp[3][0]=true;选 5 →dp[3][6]=true(1+5)。两路皆通,返回 true

一维正序 vs 倒序(num=5演示)

正序(错误示范),j从1扫到 11:

j=6: dp[6] ← dp[1] = true (凑6 = 5 + 1,合理) j=10: dp[10] ← dp[5] = true (但dp[5]是本轮刚更新的!) → 相当于一个5被用了两次:5+5=10

倒序(正确),j从11扫到1:

j=11: dp[11] ← dp[6](旧值,本轮尚未改动)✓ j=10: dp[10] ← dp[5](仍是上一行的旧值)→ 用的是"还没选过这个5"的状态

这五行的推导,就是0-1背包与完全背包的分水岭


💻 代码实现(Python + Java,二维 + 一维)

Python版

classSolution:# ===== 解法一:二维DP =====defcanPartition2D(self,nums:List[int])->bool:total=sum(nums)iftotal%2:# 奇数和必不能平分returnFalsetarget=total//2n=len(nums)dp=[[False]*(target+1)for_inrange(n+1)]dp[0][0]=True# 0 个数字凑出 0foriinrange(1,n+1):num=nums[i-1]forjinrange(target+1):dp[i][j]=dp[i-1][j]# 不选 numifj>=num:# 选 numdp[i][j]|=dp[i-1][j-num]returndp[n][target]# ===== 解法二:一维优化(倒序)推荐 =====defcanPartition(self,nums:List[int])->bool:total=sum(nums)iftotal%2:returnFalsetarget=total//2dp=[False]*(target+1)dp[0]=True# 容量0恒可达fornuminnums:# 外层:物品forjinrange(target,num-1,-1):# 内层:容量【倒序】!dp[j]=dp[j]ordp[j-num]# dp[j-num] 是上一行旧值returndp[target]

Java版

classSolution{// ===== 解法一:二维 DP =====publicbooleancanPartition2D(int[]nums){inttotal=0;for(intx:nums)total+=x;if(total%2!=0)returnfalse;inttarget=total/2,n=nums.length;boolean[][]dp=newboolean[n+1][target+1];dp[0][0]=true;for(inti=1;i<=n;i++){intnum=nums[i-1];for(intj=0;j<=target;j++){dp[i][j]=dp[i-1][j];if(j>=num)dp[i][j]|=dp[i-1][j-num];}}returndp[n][target];}// ===== 解法二:一维优化(倒序)推荐 =====publicbooleancanPartition(int[]nums){inttotal=0;for(intx:nums)total+=x;if(total%2!=0)returnfalse;inttarget=total/2;boolean[]dp=newboolean[target+1];dp[0]=true;for(intnum:nums){// 外层:物品for(intj=target;j>=num;j--){// 内层:容量【倒序】!dp[j]=dp[j]||dp[j-num];}}returndp[target];}}

⚠️致命坑(必看)

  • 一维优化时,内层容量必须倒序——正序会让dp[j-num]混入本轮已选当前物品的脏数据。
  • 外层物品、内层容量,顺序不能反。
  • 奇数和直接剪枝,省一半时间。

⏱️ 复杂度分析(面试必问)

版本时间空间
二维DPO(n × target)O(n × target)
一维优化O(n × target)O(target)

target = sum/2。本题n ≤ 200、target ≤ 10000,约200万次运算,轻松通过。相比暴力O(2^n),这是DP的降维打击。


🚀 举一反三:3 道高频变种题

题目变化点思路调整
LC.494 目标和每个数加 +/- 使结果为S转化为求子集和为(sum+S)/2的方案数,一维倒序改计数:dp[j] += dp[j-num]
LC.1049 最后一块石头的重量II两两相撞求最小剩余转化为容量sum/2的0-1背包求能装的最大和
LC.698 划分为k个相等的子集分k份而非2份背包模型失效,用状态压缩 + 回溯

💬 面试追问模拟(提前准备,惊艳全场)

Q1:一维为什么倒序?正序会怎样?

正序时dp[j-num]在同一轮已被更新,混入了“当前物品已选”的信息,等于允许每件物品选无限次(0-1背包悄悄变成完全背包),可能凭空产出false→true的错误结论;倒序保证用到的是上一行旧值,每件物品恰好决策一次。

Q2:背包问题都能问什么?

经典四问:

最大值(容量内最大价值);

最小值(装满最少件数);

存在性(能否恰好装满,如本题);

方案数(恰好装满有几种方式)。

四问共用“状态 = 物品 × 容量”的骨架,只换转移算子:max / min / or / +。

Q3:有没有更快的判定法?

存在性问题上界就是 O(n·target),但有bitset位优化:把dp数组看成一个整数,转移即bits |= bits << num,利用机器字长64位并行,理论复杂度除以64。Python 一行bits |= bits << num也极优雅。


🧩 实战小技巧(刷题党必备)

  • 口诀:0-1背包倒序跑,完全背包正序来;物品外层容量内,选与不选两条路。
  • 模板:存在性dp[j] = dp[j] || dp[j-num];方案数dp[j] += dp[j-num];最值dp[j] = max/min(dp[j], dp[j-num] + val)
  • 防坑:一维优化内层倒序,忘一次错一次。

📈 实际应用场景(不止是刷题)

  • 资源分配:服务器内存能否恰好切分满足两批任务
  • 打包装箱:货物能否对半分给两辆车
  • 编译器寄存器分配:简化模型
  • NP-hard问题:0-1背包是著名的“伪多项式可解”代表

🎁 今日思考题

如果题目改成“能否分成两个子集,使它们的差最小”,你会怎么改?
提示:转化为容量sum/2 的0-1背包,求能装到的最大和,答案 = sum - 2 × 最大和。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 7:55:57

数据中台全生命周期管理实战:从需求调研到迭代退出

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 7:55:13

移动端图形优化:纹理压缩与后处理降带宽,从根源解决发热掉帧

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 7:55:09

网站蜘蛛爬行统计系统搭建指南:保姆级建站教程避坑

网站蜘蛛爬行统计系统搭建指南:保姆级建站教程避坑 改个需求建站公司拖一周,最后交出来的东西连个像样的日志都看不到。这种憋屈感,很多做站的朋友都懂。你以为花了钱买了服务,其实买到的只是“黑盒”。今天这篇 保姆级建站教程 不吹嘘高大上的架构,专门讲怎么给 网站蜘蛛爬行统计系统…

作者头像 李华
网站建设 2026/9/15 7:55:06

收藏!小白也能入门:掌握AI大模型,高薪岗位等你来!

本文探讨了AI大模型应用开发工程师的兴起及其高薪原因。企业更关注如何让现有大模型解决实际问题&#xff0c;如搭建知识库、开发智能客服等&#xff0c;而非模型训练。AI行业价值正在从模型研究转向应用开发&#xff0c;对具备项目经验和工程能力的人才需求激增。许多学习了AI…

作者头像 李华
网站建设 2026/9/15 7:54:01

邮箱格式校验:从正则到RFC 5322的分层验证实践

1. 内容整体设计与思路拆解1.1 为什么不能信网上流传的邮箱正则我最早做邮箱校验的时候&#xff0c;跟大多数人一样&#xff0c;直接打开搜索引擎&#xff0c;找一条所谓“万能邮箱正则”&#xff0c;复制粘贴到项目里就完事。直到某天生产环境里收到一个投诉&#xff1a;用户说…

作者头像 李华
网站建设 2026/9/15 7:53:51

米花营销宝2.0.7源码深度拆解:PHP+MySQL构建裂变营销系统

简介&#xff1a;米花营销宝2.0.7源码是一套面向微信生态的 H5 营销工具源码&#xff0c;定位为帮助企业或个人快速搭建九宫格抽奖、大转盘、摇一摇、答题红包、红包海报及文章营销等互动场景&#xff0c;覆盖拉新促活、品牌曝光和销售转化等常见运营需求。这套源码压缩包共 80…

作者头像 李华