Skip to content

入门:序列 DP 与区间 DP 的分野

基于通用算法概念 · 核于 2026-07

速查

  • 序列 DP:状态是 dp[i],语义「前 i 个元素(或以 i 结尾)的最优」——一维递推。代表:LIS(以 i 结尾的最长递增子序列)、Kadane 最大子数组和、最长回文子序列(虽是二维但本质序列)。
  • 区间 DP:状态是 dp[i][j],语义「区间 [i, j] 内的最优」——按区间长度从小到大枚举,内部枚举断点 k。代表:最长回文子串、石子合并、戳气球、矩阵连乘。
  • LCS(最长公共子序列)dp[i][j] = s1[i]==s2[j] ? dp[i-1][j-1]+1 : max(dp[i-1][j], dp[i][j-1]),O(nm)。
  • LIS(最长递增子序列):朴素 O(n²)(dp[i]=max(dp[j])+1, a[j]<a[i]);二分优化 O(n log n)(维护一个「尾元素最小的递增子序列」数组,二分找第一个 ≥x 的位置替换)。
  • 编辑距离dp[i][j] = s1[i]==s2[j] ? dp[i-1][j-1] : 1+min(增、删、改),O(nm)。
  • 最长回文子串(区间 DP)dp[i][j]s[i..j] 是否回文;len<=2 看两端,len>2dp[i+1][j-1] && s[i]==s[j]
  • 石子合并(区间 DP)dp[i][j]=min(dp[i][k]+dp[k+1][j])+sum[i..j]k 枚举断点,前缀和算区间和,O(n³)。
  • 区间 DP 枚举顺序外层枚举区间长度 len(从小到大),中层枚举左端 i,算出 j=i+len-1,内层枚举断点 k——保证算 dp[i][j] 时它的子区间 dp[i][k]dp[k+1][j] 已就绪。
  • 状态定义技巧:①问「前 i 个」用 dp[i];②问「区间」用 dp[i][j];③涉及两序列用 dp[i][j](i、j 各管一序列);④问「以 i 结尾」用 dp[i] + 内层枚举 j(如 LIS)。
  • 空间优化:LCS/编辑距离可压一维(滚动数组,注意 dp[i-1][j-1] 要用临时变量暂存);LIS 二分版只需一个一维数组;区间 DP 难压(依赖 i+1 行和同行左侧)。
  • 与基础 DP 的衔接:基础 DP(背包)的状态是「前 i 个物品 + 容量 j」;本叶把「容量」换成「序列下标」或「区间右端」,套路完全一致——状态定义 + 转移方程 + 计算顺序
  • 进阶顺序LCS 与 LIS编辑距离与区间 DP参考

一、序列 DP:状态落在「前 i 个」或「以 i 结尾」

序列 DP 的状态是一维的 dp[i],它的语义无外乎两种:

  1. 「前 i 个元素」的最优dp[i] 表示考虑了 a[0..i-1] 后的答案。转移往往从 dp[i-1] 来(是否纳入 a[i])。
  2. 「以 a[i] 结尾」的最优dp[i] 表示必须以第 i 个元素结尾的最优解。转移要枚举前驱 j<i(如 LIS 的 dp[i] = max(dp[j])+1)。

两者区别在于「是否强制纳入当前元素」。LIS 是典型的「以 i 结尾」:dp[i] 表示以 a[i] 结尾的最长递增子序列长度,必须枚举所有 j<ia[j]<a[i] 来扩展。Kadane 最大子数组和也是「以 i 结尾」:dp[i] = max(a[i], dp[i-1]+a[i]),要么单开一段,要么接在上一段后面。

新手最容易犯的错:把「以 i 结尾」和「前 i 个」混为一谈。判别技巧:题目问「最长子序列/子数组」时,若要求连续,多半是「以 i 结尾」(Kadane);若不要求连续,LIS 那种也是「以 i 结尾」但内层枚举 j。而「前 i 个」多用于「能否凑出 / 方案数」类(如爬楼梯、打家劫舍)。

二、区间 DP:状态落在「区间 [i, j]」

区间 DP 处理的是「对一个区间做合并/消除/分割」的问题。状态是 dp[i][j],表示区间 [i, j] 上的最优解(最小代价、最长长度、是否能……)。它的精髓在于:大区间的解,来自「在某断点 k 切一刀」后的两个小区间的解的组合

区间 [i, j] 在断点 k 处一分为二:
[i, j] → [i, k] + [k+1, j]
dp[i][j] = 组合(dp[i][k], dp[k+1][j])   // k 从 i 到 j-1 枚举

这要求:dp[i][j] 时,dp[i][k]dp[k+1][j] 必须已经算好——而它们的区间长度都严格小于 [i, j]。所以区间 DP 的枚举顺序是**「区间长度从小到大」**:

for len in 2..n:              // 外层:区间长度(小→大)
  for i in 0..n-len:           // 中层:左端点
    j = i + len - 1            // 算出右端点
    for k in i..j-1:           // 内层:枚举断点
      dp[i][j] = optimize(dp[i][j], combine(dp[i][k], dp[k+1][j]))

这是区间 DP 的万能骨架。石子合并、戳气球、矩阵连乘、最长回文子串(的区间版)都套这个结构。最高频 bug:外层不按长度而按下标 i 枚举——会用到尚未计算的子区间,得到错误答案。

三、三大经典一眼速记

模型状态转移核心复杂度
LCSdp[i][j](前 i、前 j)字符相等 +1,否则取左/上 maxO(nm)
LISdp[i](以 i 结尾)max(dp[j])+1, a[j]<a[i];二分优化O(n²) / O(n log n)
编辑距离dp[i][j](前 i、前 j)字符相等免操作,否则 1+min(增,删,改)O(nm)
最长回文子串dp[i][j](区间是否回文)两端相等且内部回文O(n²)
石子合并dp[i][j](区间最小代价)min(dp[i][k]+dp[k+1][j])+sumO(n³)

注意 LCS 和编辑距离虽然状态都是 dp[i][j],但它们不是区间 DP——它们的 i、j 分别管两个序列(双序列 DP),不是「区间两端」。真正的区间 DP 是 i、j 管同一个序列/数组的两端。

四、状态定义的四把钥匙

DP 题难就难在「怎么定义状态」。这里给四条启发式:

  1. 问「前 i 个 / 前 i 个与容量 j」→ 一维或二维序列 DP:爬楼梯、打家劫舍、背包(容量是第二维)。
  2. 问「以第 i 个结尾的最优」→ 一维 + 内层枚举前驱:LIS、Kadane、最大乘积子数组。
  3. 问「两序列的关系」→ 二维 dp[i][j],i 管 s1 前 i、j 管 s2 前 j:LCS、编辑距离、正则匹配、不同的子序列。
  4. 问「一个区间上的合并/消除/分割」→ 区间 DP dp[i][j],按长度枚举 + 枚举断点:石子合并、戳气球、最长回文子串。

记住这四把钥匙,90% 的序列/区间 DP 题都能对号入座。

五、本叶与三部曲的坐标

  • 基础(背包):状态「前 i 物品 + 容量 j」,选或不选——是 DP 的「Hello World」。
  • 本叶(序列区间):把「容量」换成「序列下标」或「区间右端」,状态变成 dp[i]dp[i][j]——是 DP 的「主力战场」。
  • 进阶(树/数位/换根):状态落在树结构或数位上,本叶的「状态定义 + 转移方程 + 计算顺序」心智模型完全平移过去。

下一步

理解了序列 DP 与区间 DP 的分野后,下一步先用** LCS 与 LIS** 把「序列 DP」吃透——它们是面试出现频率最高的两道经典,见LCS 与 LIS:序列 DP 双壁