一维 DP 通用思路与模板(计数、背包、股票买卖)

一、一维 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 或其他运算。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值