一、一维 DP 的通用思路
一维 DP(1D DP)就是在一个线性下标上做递推:用 dp[i] 表示“到位置 i 的某种最优值 / 方案数 / 可行性”,然后按顺序从小到大把表填满。labuladong
通用步骤
以线性 DP 为例,通常按这几个步骤来:
定义状态 dp[i]
- 明确 i 的含义:第 i 个元素、前 i 个数、容量为 i、金额 i 等。
- 明确 dp[i] 存什么:最大值 / 最小值 / 方案数 / 布尔(能否到达)。
写状态转移
- 找出“从哪些状态可以转移到 i”:如 i-1、i-2 或一组前驱集合。
- 典型形式:
- 最值型:dp[i] = max/min(若干 dp[prev] +/− cost)
- 计数型:dp[i] = sum(dp[prev])
- 可行性:dp[i] = OR(dp[prev] && 条件)。labuladong
初始化(边界)
- 常见如:dp[0] = 0(代价型),或 dp[0] = 1(计数型,表示“空方案”)。
- 需要时对负下标视为无效或极小 / 极大值。
确定计算顺序
- 通常 for i in 1…n,保证用到的前驱 dp[prev] 都在前面算过。
空间优化(可选)
- 若 dp[i] 只依赖有限几个前面的状态(如 i-1, i-2),可用有限变量替换数组(滚动变量 / 滚动数组)。
二、计数型一维 DP(以凑和为例)
1. 通用计数模板
计数问题典型形式:dp[i] 表示“达到状态 i 的方案数”,转移时把所有前驱方案数加起来。
伪代码模板:
# dp[i]:达到状态 i 的方案数
dp = array of length n+1
dp[0] = 1 # 初始:空方案 1 种(凑出 0 的方式)
for i from 1 to n:
dp[i] = 0
for each choice c that can reach i:
prev = index_of_previous_state(i, c)
dp[i] = dp[i] + dp[prev]
return dp[n] # 或根据题目取区间内的值 / 求和
这里的关键点是:
- dp[i] = 0 是初始化,表示“当前还没有统计任何前驱贡献”;
- 内层循环中不断 += dp[prev],相当于 dp[i]=∑所有可达 prevdp[prev]
- 如果只有一个前驱,那等价于 dp[i] = dp[prev];如果有多个,就是真正的“求和”。
2. dp[0] = 1 的含义:空方案
在很多“凑和 / 子集 / 硬币”类计数题里,dp[0] = 1 的含义是:
- 凑出和为 0 的方案数是 1,因为“什么都不选”也算一种合法方案(空集合)。hello-algo+1
关键区分两件事:
- “空集合能不能凑出和为 3?”——不能,方案数是 0。
- “空集合能不能凑出和为 0?”——能,而且只有一种方法:一个都不选。
DP 用的是第二个事实:空集合凑出 0 的方案 = 1。
之后通过“加一个元素”的转移,把“凑 0 的方案”变成“凑 3 的方案”等。
三、完全背包与一维转移
1. 完全背包与 0/1 背包的区别
- 0/1 背包:每个物品最多选 1 次。
- 完全背包:每个物品可以选无限多次(0、1、2、…)。
典型定义(价值型):
- 有 N 件物品、容量 W。
- 物品 i:重量 w[i],价值 v[i]。
- dp[j]:容量为 j 时的最大价值。oi-wiki+1
2. 一维 0/1 背包(倒序)
一维压缩后,0/1 背包转移:
for i in 0..n-1: # 遍历物品
for j from W down to w[i]: # 容量倒序
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
倒序的原因:
- 计算 dp[j] 时,dp[j - w[i]] 对应更小容量,在这一轮还没更新,是“上一轮 i-1 物品之后”的状态。
- 这样保证“每件物品在这一轮最多只被用一次”,不会重复使用同一物品 i。csdn+1
3. 一维完全背包(正序)
完全背包的价值型转移:
for i in 0..n-1: # 遍历物品
for j from w[i] to W: # 容量正序
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
正序的原因:
- 计算较大的 j 时,dp[j - w[i]] 对应较小 j,在本轮已经更新过,里面已经可能包含多个 i 物品。
- 再加一次 v[i],就相当于“在已经用过若干个 i 的基础上,再拿一个 i”,从而支持“无限次使用物品 i”。csdn+1
4. 完全背包的计数型写法(硬币凑和)
计数型完全背包:dp[j] 表示“用给定数字凑出和 j 的方案数”(组合数,不区分顺序)。
dp[0] = 1
for each num in nums: # num 类似物品重量
for j from num to target: # 从 num 开始,避免负下标
dp[j] += dp[j - num]
解释 dp[j] += dp[j - num]:
- 所有凑出 j - num 的方案,每个再加一个 num,就变成凑出 j 的方案。
- 新增方案数 = dp[j - num],所以要累加进 dp[j]。
- 这里是计数问题,不求最大值,所以用 += 而不是 max。csdn+1
j 从 num 开始而不是从 0 开始,是为了避免访问 dp[j - num] 时出现负下标;写成 for j in 0…target 也可以,只是需要加 if j >= num 判断。
四、背包代码里的遍历方向总结
一维 DP 时,外层是“物品”,内层是“容量”:
- 0/1 背包(每件最多一次):容量要 倒序
- 防止在同一轮中重复使用同一个物品。
- 完全背包(可以无限次):容量要 正序
- 刻意利用“本轮已更新的 dp[j - w]”来表示已经用过当前物品,再多用一次。csdn+1
计数 / 组合型问题时,也遵循同样的思路,只是把 max 换成 +=。
五、股票买卖:状态机式一维 DP
1. 状态定义
以“可以无限次买卖,最多持有 1 股”为例(LeetCode 股票 II 类型):labuladong+1
- hold[i]:第 i 天结束时,手里持有一股时的最大利润。
- cash[i]:第 i 天结束时,手里不持股时的最大利润。
这是一个“二维状态机”压成两个一维数组(再进一步可以压成常数个变量)。
2. 状态转移方程
对于第 i 天(价格 price[i]):
hold[i] = max(
hold[i-1], # 昨天就持股,今天不动
cash[i-1] - price[i] # 昨天不持股,今天买入
)
cash[i] = max(
cash[i-1], # 昨天就不持股,今天不动
hold[i-1] + price[i] # 昨天持股,今天卖出
)
解释关键点:
- 买入只能从“昨天不持股”的状态来,所以是 cash[i-1] - price[i],而不能是 hold[i-1] - price[i](那是“一天买两股”)。
- 卖出只能从“昨天持股”的状态来,所以是 hold[i-1] + price[i]。cnblogs+1
3. 为什么不是其他形式
- hold[i] = max(hold[i-1], hold[i-1] - price[i])
- 第二项相当于“昨天已经持股,今天再买一股”,违背“最多持有一股”的约束。
- hold[i] = max(hold[i-1], cash[i] - price[i])
- 用 cash[i] 再回推 hold[i] 打乱了时间顺序(自引用),转移必须是从第 i-1 天到第 i 天,而不是在同一天之间互跳。labuladong
4. 初始条件与滚动变量写法
初始化(以第 0 天为起点):
- cash[0] = 0:第 0 天不持股,利润为 0。
- hold[0] = -price[0]:如果允许第 0 天买入,那么持股状态利润为 -price[0]。eliasyaoyc.github+1
使用滚动变量压缩:
# prices: 长度为 n 的数组
hold = -10**18 # 类似 -∞,表示不可能在“第 0 天之前”就持股
cash = 0 # 初始不持股,利润 0
for price in prices:
new_hold = max(hold, cash - price) # 今天结束时持股
new_cash = max(cash, hold + price) # 今天结束时不持股
hold, cash = new_hold, new_cash
answer = cash # 最后一天不持股的最大利润
这里的思想是:
- hold / cash 始终对应“上一天结束时”的两个状态;
- 用当前 price 计算“今天结束时”的新状态 new_hold / new_cash;
- 再整体赋值回去,像状态机一样每天推进。vocus+1
5. 关于 hold[i] 和 cash[i] 的大小关系
- hold[i] 与 cash[i] 是不同前提下的最优值(持股 vs 不持股),本身不要求某个一定大于另一个。
- 最终答案取 cash[last],是因为题目要求的是“整个交易结束时手里没有股票的最大利润”。
- 中途某天 cash[k] 最大的话,由于转移中有“不操作”这一分支,后面的 cash[k+1…n-1] 至少不小于它,最后 cash[n-1] 等价于 max(cash[0…n-1])。labuladong
六、小结与实践建议
先分清问题类型
- 最值型:max / min;
- 计数型:+ 累加;
- 可行性:布尔 OR / AND。
背包方向记忆
- 0/1 背包:容量倒序,防止重复用同一物品。
- 完全背包:容量正序,允许重复用物品。csdn+1
计数问题中的 dp[0] = 1
- 表示“凑出 0 的空方案有 1 种”,后续所有方案都从这条空路径扩展而来。hello-algo+1
状态机 DP 的模式
- 明确每个状态的“物理含义”(如:持股 / 不持股、是否用过某操作)。
- 写出从上一天 / 上一步所有状态转到当前状态的方式,统一用 max 或其他运算。
&spm=1001.2101.3001.5002&articleId=156289320&d=1&t=3&u=2ddff3b8b76b42749b4fe9f613fba594)
519

被折叠的 条评论
为什么被折叠?



