```mermaid graph LR A[Mermaid Keeper] --> B[保持激活] ```

前言

上一篇 LIS、LCS 与编辑距离 里,最长回文子序列已经用到了一种特殊的状态——dp[l][r]dp[l][r] 表示”区间 [l,r][l, r] 上的最优解”。这种按区间定义状态、从小区间推导大区间的 DP,就是区间 DP

区间 DP 的应用场景很广。比如:把一串数字按相邻对删掉,最小代价是多少?给一串括号,最少补几个才能匹配?把若干矩阵相乘,怎样加括号使得乘法次数最少?这些问题都共享同一个结构——在某处切一刀,左半边和右半边各自最优,再合并

问题的本质

为什么普通 DP 不够用?

之前遇到的 DP,状态都是”前 ii 个”或”容量 jj“——状态沿着一个方向增长。但有些问题的状态天然是一个区间。比如回文子序列,S[l]S[l]S[r]S[r] 能不能匹配取决于两端字符,不是”前缀”能描述的。

区间 DP 的核心思想:先解决所有小区间的最优解,再逐步扩大区间长度。计算顺序是按区间长度从小到大,不是按位置从左到右。

通用框架

dp[l][r]dp[l][r] = 区间 [l,r][l, r] 上的最优值。枚举分割点 k[l,r)k \in [l, r)

dp[l][r]=mink(dp[l][k]+dp[k+1][r]+合并代价)dp[l][r] = \min_{k} (dp[l][k] + dp[k+1][r] + \text{合并代价})

// 初始化:长度为 1 的区间
for (int i = 0; i < N; i++) dp[i][i] = 初始值;

// 按区间长度从小到大枚举
for (int len = 2; len <= N; len++)           // ① 先算短区间
    for (int l = 0; l + len - 1 < N; l++) {  // ② 枚举左端点
        int r = l + len - 1;                  // ③ 右端点
        for (int k = l; k < r; k++)           // ④ 枚举分割点
            dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r] + cost);
    }

逐行解析

  • 区间长度必须从小到大。因为计算长度为 lenlen 的区间时,需要用到所有长度 <len< len 的区间的结果。
  • ②③ 给定长度 lenlen 和左端点 ll,右端点 r=l+len1r = l + len - 1 就确定了。
  • ④ 在 [l,r][l, r] 内枚举分割点 kk,把区间分成 [l,k][l, k][k+1,r][k+1, r] 两半。两半各自已经算好了,加上合并代价就是总代价。

时间复杂度 O(N3)O(N^3)NN 个左端点 ×\times NN 个右端点 ×\times NN 个分割点)。N500N \le 500 以内可以接受。

理论 + 代码

模型一:相邻配对删除

给定 2N2N 个数排成一排。每次删相邻两个数,代价是 AiAi+1|A_i - A_{i+1}|。删完所有数的最小总代价?

这不是简单的贪心——每次删最小代价的相邻对可能导致后续配对变差。

状态dp[l][r]dp[l][r] = 把区间 [l,r][l, r] 中的数全部删掉的最小代价。

转移:枚举 ll 和谁配对。如果 llkk 配对(l<krl < k \le r),代价是 AlAk|A_l - A_k|,然后 [l+1,k1][l+1, k-1][k+1,r][k+1, r] 各自删完:

dp[l][r]=mink=l+1,l+3,(AlAk+dp[l+1][k1]+dp[k+1][r])dp[l][r] = \min_{k=l+1,l+3,\ldots} (|A_l - A_k| + dp[l+1][k-1] + dp[k+1][r])

注意 kk 的奇偶性:llkk 之间有奇数个元素时才能全部配对,所以 klk - l 必须是奇数。

模型二:回文子序列(区间 DP 版)

上一篇已经讲过。dp[l][r]dp[l][r] = 子串 S[lr]S[l \ldots r] 的最长回文子序列长度。

  • S[l]=S[r]S[l] = S[r]dp[l][r]=dp[l+1][r1]+2dp[l][r] = dp[l+1][r-1] + 2
  • S[l]S[r]S[l] \ne S[r]dp[l][r]=max(dp[l+1][r], dp[l][r1])dp[l][r] = \max(dp[l+1][r],\ dp[l][r-1])

模型三:矩阵链乘法

NN 个矩阵 M1,M2,,MNM_1, M_2, \ldots, M_NMiM_i 大小 ri×cir_i \times c_i,且 ci=ri+1c_i = r_{i+1}。求最少乘法次数。

状态dp[l][r]dp[l][r] = 矩阵 MlM_lMrM_r 相乘的最少乘法次数。

转移:最后一步一定是某次乘法把 [l,k][l, k][k+1,r][k+1, r] 合并:

dp[l][r]=minlk<r(dp[l][k]+dp[k+1][r]+rl×ck×cr)dp[l][r] = \min_{l \le k < r} (dp[l][k] + dp[k+1][r] + r_l \times c_k \times c_r)

其中 rl×ck×crr_l \times c_k \times c_r 是最后那次矩阵乘法的代价。

例题

例题 1:T90 019 — Pick Two(★6,相邻配对删除)

题目:长度 2N2N 的数列 AA。每次选相邻两个数删掉,代价 AiAi+1|A_i - A_{i+1}|。重复 NN 次删完所有数。求最小总代价。

数据范围1N151 \le N \le 151Ai1051 \le A_i \le 10^5

—— AtCoder Typical 90 019

分析2N302N \le 30O(N3)O(N^3) 的区间 DP 绰绰有余。

状态 dp[l][r]dp[l][r] = 把 A[lr]A[l \ldots r] 全部删完的最小代价。rl+1r - l + 1 必须是偶数(每次删 2 个)。

转移:枚举 ll 和谁配对。配对后,中间和右边各自独立删除。

输入样例

3
6 2 3 9 8 6

输出2

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int INF = 1e9;
int A[36], dp[36][36];

int main() {
    int N;
    scanf("%d", &N);
    for (int i = 0; i < 2 * N; i++) scanf("%d", &A[i]);

    for (int i = 0; i < 2 * N; i++)
        for (int j = 0; j < 2 * N; j++) dp[i][j] = INF;
    for (int i = 0; i < 2 * N; i++) dp[i][i] = 0;   // ① 单个元素无法删除

    // ② 按区间长度枚举(只考虑偶数长度)
    for (int len = 2; len <= 2 * N; len += 2)
        for (int l = 0; l + len - 1 < 2 * N; l++) {
            int r = l + len - 1;
            // ③ 枚举 l 和谁配对
            for (int k = l + 1; k <= r; k += 2) {
                int cost = abs(A[l] - A[k]);
                int mid = (l + 1 <= k - 1) ? dp[l+1][k-1] : 0;
                int right = (k + 1 <= r) ? dp[k+1][r] : 0;
                dp[l][r] = min(dp[l][r], cost + mid + right);
            }
        }
    printf("%d\n", dp[0][2*N-1]);
}

逐行解析

  • ① 单个元素不可能被删除(每次删 2 个),但设为 0 作为递推基础。
  • ② 区间长度只枚举偶数——奇数长度的区间不可能被全部删完。
  • llkk 配对,klk-l 为奇数。abs(A[l]-A[k]) 是配对代价,dp[l+1][k-1] 是中间部分删完的代价,dp[k+1][r] 是右边删完的代价。

模拟A=[6,2,3,9,8,6]A = [6, 2, 3, 9, 8, 6]N=3N=3)。

长度 2 的区间:dp[0][1]=62=4dp[0][1]=|6-2|=4, dp[1][2]=23=1dp[1][2]=|2-3|=1, dp[2][3]=39=6dp[2][3]=|3-9|=6, dp[3][4]=98=1dp[3][4]=|9-8|=1, dp[4][5]=86=2dp[4][5]=|8-6|=2

长度 4 的区间:

  • dp[0][3]dp[0][3](0,1)(0,1) 配+ dp[2][3]=6dp[2][3]=64+6=104+6=10;或 (0,3)(0,3) 配+ dp[1][2]=1dp[1][2]=169+1=4|6-9|+1=4。取 4。
  • dp[1][4]dp[1][4](1,2)(1,2) 配+ dp[3][4]=1dp[3][4]=11+1=21+1=2;或 (1,4)(1,4) 配+ dp[2][3]=6dp[2][3]=628+6=12|2-8|+6=12。取 2。
  • dp[2][5]dp[2][5](2,3)(2,3) 配+ dp[4][5]=2dp[4][5]=26+2=86+2=8;或 (2,5)(2,5) 配+ dp[3][4]=1dp[3][4]=136+1=4|3-6|+1=4。取 4。

长度 6 的区间 dp[0][5]dp[0][5]

  • (0,1)(0,1) 配+ dp[2][5]=4dp[2][5]=44+4=84+4=8
  • (0,3)(0,3) 配+ dp[1][2]+dp[4][5]=1+2=3dp[1][2]+dp[4][5]=1+2=369+3=6|6-9|+3=6
  • (0,5)(0,5) 配+ dp[1][4]=2dp[1][4]=266+2=2|6-6|+2=\mathbf{2}

答案 = 2。✓


例题 2:TB B21 — Longest Subpalindrome(最长回文子序列)

题目:长度 NN 的字符串 SS,求最长回文子序列的长度。

数据范围1N10001 \le N \le 1000

—— AtCoder Tessoku Book B21

分析:教材 4.6 节的区间 DP。状态 dp[l][r]dp[l][r] = 子串 S[lr]S[l \ldots r] 中最长回文子序列长度。

代码

#include <cstdio>
#include <algorithm>
using namespace std;

int dp[1009][1009];

int main() {
    int N;
    char S[1009];
    scanf("%d%s", &N, S);

    // ① 长度 1:单字符回文
    for (int i = 0; i < N; i++) dp[i][i] = 1;
    // ② 长度 2:相邻两字符
    for (int i = 0; i < N - 1; i++)
        dp[i][i+1] = (S[i] == S[i+1]) ? 2 : 1;

    // ③ 按区间长度递增
    for (int len = 2; len < N; len++)
        for (int l = 0; l + len < N; l++) {
            int r = l + len;
            if (S[l] == S[r])
                dp[l][r] = dp[l+1][r-1] + 2;        // ④ 两端匹配
            else
                dp[l][r] = max(dp[l+1][r], dp[l][r-1]); // ⑤ 去掉一端
        }
    printf("%d\n", dp[0][N-1]);
}

逐行解析

  • ①② 边界:单字符回文长 1,双字符看是否相同。
  • ③ 区间 DP 的核心——按长度递增计算。
  • S[l]=S[r]S[l] = S[r] 时两端同时选入回文,中间部分的最优解来自 dp[l+1][r1]dp[l+1][r-1]
  • ⑤ 两端不同,至少有一端不选,取去掉左端或右端的较大值。

模拟SS = “character”(N=9N=9)。

len关键更新
1dp[i][i] = 1
2dp[2][3]=‘ra’不匹配→1, dp[4][5]=‘cc’匹配→2
3dp[0][2]=‘cha’→max(dp[1][2],dp[0][1])=1, dp[4][6]=‘cac’→dp[5][5]+2=3
8dp[0][8] → 最长回文子序列 “caac” 或 “caaac” 等长度

最终 dp[0][8] = 5(如 “caaac”)。具体回文内容取决于字符串,但算法正确性不依赖具体还原。


例题 3:括号匹配——最少补几个括号

场景:给定长度 NN 的括号序列 SS(只含 ()),求最少需要插入多少个括号才能使序列合法。

数据范围1N5001 \le N \le 500

分析:等价于求最长合法括号子序列。合法括号序列要求:(A)B 形式,其中 AABB 也是合法的。

状态dp[l][r]dp[l][r] = 子串 S[lr]S[l \ldots r] 中最长合法括号子序列的长度。

转移

  • 如果 S[l]S[l]S[r]S[r] 匹配(()),dp[l][r]=dp[l+1][r1]+2dp[l][r] = dp[l+1][r-1] + 2
  • 枚举分割点:dp[l][r]=maxk(dp[l][k]+dp[k+1][r])dp[l][r] = \max_{k}(dp[l][k] + dp[k+1][r])

代码

#include <cstdio>
#include <algorithm>
using namespace std;

int dp[509][509];

int main() {
    int N;
    char S[509];
    scanf("%d%s", &N, S);

    // ① 按区间长度递增
    for (int len = 2; len <= N; len++)
        for (int l = 0; l + len - 1 < N; l++) {
            int r = l + len - 1;
            // ② 如果两端匹配
            if (S[l] == '(' && S[r] == ')')
                dp[l][r] = dp[l+1][r-1] + 2;
            // ③ 枚举分割点
            for (int k = l; k < r; k++)
                dp[l][r] = max(dp[l][r], dp[l][k] + dp[k+1][r]);
        }
    // ④ 需要补充的括号数 = 总长度 - 最长合法子序列长度
    printf("%d\n", N - dp[0][N-1]);
}

逐行解析

  • ② 两端是 () 时可以配对,长度 +2。
  • ③ 枚举分割点,取最优的切法。
  • ④ 最长合法子序列不需要补充,其余每个字符需要补一个对应的括号。

例题 4:矩阵链乘法

场景NN 个矩阵 M1,,MNM_1, \ldots, M_NMiM_i 的大小为 Ri1×RiR_{i-1} \times R_i。求最少乘法次数。

数据范围1N5001 \le N \le 500

分析:经典的区间 DP。dp[l][r]dp[l][r] = 矩阵 MlMrM_l \ldots M_r 连乘的最少乘法次数。

转移dp[l][r]=minlk<r(dp[l][k]+dp[k+1][r]+Rl1×Rk×Rr)dp[l][r] = \min_{l \le k < r}(dp[l][k] + dp[k+1][r] + R_{l-1} \times R_k \times R_r)

最后合并时,左边结果是 Rl1×RkR_{l-1} \times R_k 的矩阵,右边是 Rk×RrR_k \times R_r 的矩阵,乘法次数 Rl1×Rk×RrR_{l-1} \times R_k \times R_r

代码

#include <cstdio>
#include <algorithm>
using namespace std;

const long long INF = 1e18;
long long dp[509][509];
int R[509];

int main() {
    int N;
    scanf("%d", &N);
    for (int i = 0; i <= N; i++) scanf("%d", &R[i]); // R[0]..R[N]

    for (int i = 1; i <= N; i++) dp[i][i] = 0;         // ① 单个矩阵不用乘
    for (int len = 2; len <= N; len++)
        for (int l = 1; l + len - 1 <= N; l++) {
            int r = l + len - 1;
            dp[l][r] = INF;
            for (int k = l; k < r; k++)
                dp[l][r] = min(dp[l][r],
                    dp[l][k] + dp[k+1][r] + 1LL * R[l-1] * R[k] * R[r]); // ② 合并代价
        }
    printf("%lld\n", dp[1][N]);
}

逐行解析

  • ① 单个矩阵不需要乘法,代价 0。
  • MlMkM_l \ldots M_k 的结果是 Rl1×RkR_{l-1} \times R_kMk+1MrM_{k+1} \ldots M_r 的结果是 Rk×RrR_k \times R_r,两者相乘需要 Rl1×Rk×RrR_{l-1} \times R_k \times R_r 次乘法。

模拟N=3N=3,矩阵大小 10×10010 \times 100100×5100 \times 55×505 \times 50R=[10,100,5,50]R = [10, 100, 5, 50]

dp[1][2]=10×100×5=5000dp[1][2] = 10 \times 100 \times 5 = 5000dp[2][3]=100×5×50=25000dp[2][3] = 100 \times 5 \times 50 = 25000dp[1][3]dp[1][3]

  • k=1k=1dp[1][1]+dp[2][3]+10×100×50=0+25000+50000=75000dp[1][1] + dp[2][3] + 10 \times 100 \times 50 = 0 + 25000 + 50000 = 75000
  • k=2k=2dp[1][2]+dp[3][3]+10×5×50=5000+0+2500=7500dp[1][2] + dp[3][3] + 10 \times 5 \times 50 = 5000 + 0 + 2500 = \mathbf{7500}

答案 = 7500。先算前两个再算最后一个更优。✓

参考文献

教材讲解 — 競技プログラミングの鉄則 第 4 章

系统练习 — 競技プログラミングの鉄則

实战练习 — 競プロ典型 90 問


系列索引

第零章 基础工具

第一章 搜索技术

第二章 数学基础

第三章 数据结构

第四章 图论

第五章 动态规划

第六章 贪心

第七章 字符串

第八章 进阶