DP 系列(二):线性 DP

本讲定位:上一讲我们建立了”递归 → 记忆化 → 递推”的思维路径,例题的状态都”长在题面上”(行号、列号)。本讲开始自己设计状态,主角是一维序列上的三大经典模型:最大子段和、最长上升子序列(LIS)、最长公共子序列(LCS)。它们是后续一切 DP 的词汇表。

前置知识:专题一(递归到递推);lower_bound 的用法(进阶部分用到)。

标注 ⭐ 的小节为进阶内容(提高组),第一遍学习可跳过。


1. 什么是线性 DP

线性 DP:状态沿着序列的下标”一维地”推进,$f[i]$ 只依赖前面的 $f[j]\ (j < i)$。它是形态最简单的 DP,但覆盖了状态设计中最重要的一个套路:

“以 $i$ 结尾”三个字,是线性 DP 的万能钥匙。

为什么?直接定义”前 $i$ 个数的答案”往往无法转移——因为不知道前面的方案”接不接得上”第 $i$ 个数。而”以 $i$ 结尾”把接口固定住了:想接上 $a_i$,只需检查结尾元素。状态定义中多说死一点,转移就少猜一点——这是设计状态的第一课。


2. 例题一:最大子段和(洛谷 P1115)

题意:给长度为 $n$($n \le 2 \times 10^5$)的整数序列,求一段连续非空子段,使其和最大。

样例:2 -4 3 -1 2 -4 3 → 答案 4(子段 $3, -1, 2$)。

错误的起手式:设 $f[i]$ 为”前 $i$ 个数中的最大子段和”。转移时想接上 $a_i$,却不知道最优子段是否恰好停在 $i-1$——状态描述太松,接不上。

正确的状态:

$$ f[i] = \text{以 } a_i \text{ 结尾的最大子段和} $$

以 $a_i$ 结尾的子段只有两种形态:要么是 $a_i$ 自己单独一段,要么是”以 $a_{i-1}$ 结尾的某个子段”再接上 $a_i$。要最大,前者取 $a_i$,后者取 $f[i-1] + a_i$:

$$ f[i] = \max(f[i-1] + a_i,\ a_i) $$

最终答案是 $\max_i f[i]$(最优子段总得以某个位置结尾)。由于 $f[i]$ 只依赖 $f[i-1]$,连数组都可以省掉:

#include<bits/stdc++.h>
using namespace std;

int main() {
    int n;
    scanf("%d", &n);
    int f = 0, ans = INT_MIN;
    for (int i = 1; i <= n; i++) {
        int x;
        scanf("%d", &x);
        f = (i == 1) ? x : max(f + x, x);   // f:以第 i 个数结尾的最大子段和
        ans = max(ans, f);
    }
    printf("%d\n", ans);
    return 0;
}

复杂度 $O(n)$。经典陷阱:子段非空,全负数时答案是最大的那个负数,不是 $0$——所以 ans 初值必须是 INT_MIN,且不能把 f 与 $0$ 取 $\max$。


3. 例题二:最长上升子序列 LIS(洛谷 B3637)