区间DP

区间DP

注:本文是笔者在学习区间DP时,基于左程云、NotOnlySuccess、灵茶山艾府老师们的教学视频记录的学习笔记,这里特列出各位老师,以表对各位老师付出的感谢。另外,目前本文的是AI基于笔者想法产出,内容经过笔者审核,后续会进行人工更新。

1. 定义与核心思想

区间DP将大范围的问题拆分成若干小范围的问题来求解。它适用于"一段区间上的最优解可由其内部更小区间的最优解合并得到"的场景。

状态设计dp[i][j] 表示区间 [i, j] 上的最优解(最大值 / 最小值 / 方案数等)。

核心转移框架:按区间长度从小到大递推。

// 基础区间DP框架 O(n^3)
for (int len = 2; len <= n; len++) {          // 枚举区间长度
    for (int i = 1; i + len - 1 <= n; i++) {  // 枚举左端点
        int j = i + len - 1;                   // 计算右端点
        for (int k = i; k < j; k++) {          // 枚举划分点
            dp[i][j] = merge(dp[i][k], dp[k+1][j]);
        }
    }
}

实际编码中更常用"倒序枚举左端点 l,正序枚举右端点 r"的写法,配合二维 vector 实现,简洁不易出错。

时间复杂度:通常是 \(O(n^{3})\)(长度 × 左端点 × 划分点),部分题目可优化到 \(O(n^{2})\)(四边形不等式、或只需端点转移)。

与划分DP的关系:区间DP可以看作划分DP的一种特例——划分点 k 将 [i, j] 拆成 [i, k][k+1, j] 两个子区间。但区间DP的转移方式更丰富,不限于枚举划分点,还可以从端点扩展。


2. 分类讨论

区间DP根据状态如何从小范围扩展到大范围的方式,分为以下类别。

2.1 单边区间DP

特征:每次只从区间的一端取元素。当前区间 [i, j] 的状态,由去掉最左元素 [i+1, j] 或去掉最右元素 [i, j-1] 转移而来。

洛谷 P1005 矩阵取数游戏

题目描述:有一个 n×m 的矩阵,每行独立进行游戏:每次从行首或行尾取一个数,第 k 次取数的得分为 a×2^k。求最大总得分。

题目解析:每行独立,对每一行做区间DP。dp[l][r] 表示该行 [l, r] 区间能获得的最大分数。每次取数只能取左端或右端,取第 m-(r-l) 次,分数乘上对应的 2 的幂次。

int n, m;
cin >> n >> m;
__int128_t ans = 0;
while (n--) {
    vector<int> val(m);
    for (auto &v : val) cin >> v;
    vector<vector<int>> dp(m, vector<int>(m));
    for (int l = m - 1; l >= 0; l--) {
        dp[l][l] = val[l] * (1 << m);   // 最后一次取数,权重 2^m
        for (int r = l + 1; r < m; r++) {
            int times = m - (r - l);    // 第几次取:区间外已取走 m-1-(r-l) 个
            dp[l][r] = max(
                val[l] * (1 << times) + dp[l + 1][r],
                val[r] * (1 << times) + dp[l][r - 1]
            );
        }
    }
    ans += dp[0][m - 1];
}
// 输出 ans(注意使用 __int128_t 避免溢出)

复杂度:每行 \(O(m^{2})\),总 \(O(nm^{2})\)

洛谷 P2858 Treats for the Cows G/S

题目描述:有 n 个零食排成一行,每天可以卖最左或最右的一个。第 k 天卖的零食价值为 v × k,求最大总价值。

题目解析:本质和 P1005 相同,只不过倍率是线性的而非指数。dp[l][r] 表示卖完 [l, r] 的最大收入,day = n - (r - l) 为当前是第几天。

int n;
cin >> n;
vector<int> v(n);
for (auto &x : v) cin >> x;
vector<vector<int>> dp(n, vector<int>(n, INT_MAX));
for (int l = n - 1; l >= 0; l--) {
    dp[l][l] = v[l] * n;  // 最后一天卖(第 n 天)
    for (int r = l + 1; r < n; r++) {
        int day = n - (r - l);
        dp[l][r] = max(dp[l + 1][r] + v[l] * day,
                       dp[l][r - 1] + v[r] * day);
    }
}
cout << dp[0][n - 1] << endl;

复杂度:\(O(n^{2})\)

洛谷 P3847 调整队形

题目描述:给定一个数字序列,每次可以插入或删除一个数字(代价1),也可以修改一个数字(代价1,改为任意值)。求使序列变为回文的最小操作次数。

题目解析:dp[l][r] 表示让 [l, r] 变成回文的最小代价。转移讨论三种操作:删左端(dp[l+1][r]+1)、删右端(dp[l][r-1]+1)、两端匹配(相同免费,不同代价1修改)。

int n;
cin >> n;
vector<int> c(n);
for (auto &x : c) cin >> x;
vector<vector<int>> dp(n, vector<int>(n));
for (int l = n - 1; l >= 0; l--) {
    for (int r = l + 1; r < n; r++) {
        dp[l][r] = min({
            dp[l + 1][r] + 1,                       // 删除 a[l]
            dp[l][r - 1] + 1,                       // 删除 a[r]
            dp[l + 1][r - 1] + (c[l] != c[r])       // 匹配两端
        });
    }
}
cout << dp[0][n - 1] << endl;

复杂度:\(O(n^{2})\)


2.2 左右区间DP(基于两侧端点讨论)

特征:基于两侧端点的选或不选展开——讨论区间两端元素在最终序列中的角色,通常需要加一维记录"当前在区间的哪一侧"。

洛谷 P3205 合唱队

题目描述:n 个人依次入队,每个人进入时要么站到当前队列的最左边,要么站到最右边。给定最终的排列,求有多少种可能的入队顺序。

题目解析:dp[l][r][0] 表示区间 [l, r] 形成的最终排列,最后一个人从左端(0)进入的方案数;dp[l][r][1] 表示最后一个人从右端(1)进入的方案数。转移时比较身高决定新加入的人可以站在哪侧。

int n;
cin >> n;
vector<int> h(n);
for (auto &x : h) cin >> x;
vector<vector<array<int, 2>>> dp(n, vector<array<int, 2>>(n));
const int mod = 19650827;
for (int l = n - 1; l >= 0; l--) {
    dp[l][l][0] = 1;  // 单个人只有一种方式
    for (int r = l + 1; r < n; r++) {
        // 最后进入的人在左端 l:前一步区间 [l+1, r],最后进入者在 l+1 或 r
        if (h[l] < h[l + 1]) dp[l][r][0] = (dp[l][r][0] + dp[l + 1][r][0]) % mod;
        if (h[l] < h[r])     dp[l][r][0] = (dp[l][r][0] + dp[l + 1][r][1]) % mod;
        // 最后进入的人在右端 r:前一步区间 [l, r-1],最后进入者在 l 或 r-1
        if (h[r] > h[l])     dp[l][r][1] = (dp[l][r][1] + dp[l][r - 1][0]) % mod;
        if (h[r] > h[r - 1]) dp[l][r][1] = (dp[l][r][1] + dp[l][r - 1][1]) % mod;
    }
}
cout << (dp[0][n - 1][0] + dp[0][n - 1][1]) % mod << endl;

复杂度:\(O(n^{2})\)

洛谷 P10236 排卡

题目描述:给定一个序列 a,每次从左侧或右侧取一张卡放入新序列末尾。若新序列末尾元素与前一个元素不同,则获得 a[x]^a[y] 的分数。求最大总得分。

题目解析:dp[l][r][0] 表示区间 [l, r] 最后取的是 a[l] 的最大得分,dp[l][r][1] 表示最后取的是 a[r] 的最大得分。转移时根据新取的卡和上一张卡的值计算新增分数。

int T;
cin >> T;
while (T--) {
    int n;
    cin >> n;
    vector<int> a(n);
    for (auto &x : a) cin >> x;
    const i64 inf = LLONG_MIN / 2;
    vector<vector<array<i64, 2>>> dp(n, vector<array<i64, 2>>(n, {inf, inf}));
    for (int l = n - 1; l >= 0; l--) {
        dp[l][l][0] = dp[l][l][1] = 0;
        for (int r = l + 1; r < n; r++) {
            dp[l][r][0] = max(
                dp[l + 1][r][0] + qpow(a[l], a[l + 1]),
                dp[l + 1][r][1] + qpow(a[l], a[r])
            );
            dp[l][r][1] = max(
                dp[l][r - 1][0] + qpow(a[r], a[l]),
                dp[l][r - 1][1] + qpow(a[r], a[r - 1])
            );
        }
    }
    cout << max(dp[0][n - 1][0], dp[0][n - 1][1]) << endl;
}

复杂度:\(O(n^{2})\)

洛谷 P1220 关路灯

题目描述:一条路上有 n 盏路灯,每盏灯有位置 p[i] 和功率 w[i]。老张初始在位置 c。关掉一盏灯不需要时间,但走路需要时间,未关的灯会持续消耗功率。求关完所有灯的最小总功耗。

题目解析:经典"已关的灯一定是一个连续区间"模型。dp[l][r][0] 表示已关掉 [l, r] 的灯,当前在 l 的最小功耗;dp[l][r][1] 表示当前在 r 的最小功耗。转移时计算走到下一盏灯的耗时 × 剩余未关灯的功率。

int n, c;
cin >> n >> c;
vector<int> p(n + 1), w(n + 1);
for (int i = 1; i <= n; i++) cin >> p[i] >> w[i];
vector<int> sum(n + 1);
partial_sum(w.begin(), w.end(), sum.begin());  // 功率前缀和
const int inf = INT_MAX / 2;
vector<vector<array<int, 2>>> dp(n + 1, vector<array<int, 2>>(n + 1, {inf, inf}));
dp[c][c][0] = dp[c][c][1] = 0;
for (int l = n; l >= 1; l--) {
    for (int r = l + 1; r <= n; r++) {
        // 走到左端 l:从上一步的 l+1 或 r 走来
        dp[l][r][0] = min({
            dp[l + 1][r][0] + abs(p[l + 1] - p[l]) * (sum[n] - (sum[r] - sum[l])),
            dp[l + 1][r][1] + abs(p[r] - p[l])     * (sum[n] - (sum[r] - sum[l]))
        });
        // 走到右端 r:从上一步的 l 或 r-1 走来
        dp[l][r][1] = min({
            dp[l][r - 1][0] + abs(p[l] - p[r])     * (sum[n] - (sum[r - 1] - sum[l - 1])),
            dp[l][r - 1][1] + abs(p[r - 1] - p[r]) * (sum[n] - (sum[r - 1] - sum[l - 1]))
        });
    }
}
cout << min(dp[1][n][0], dp[1][n][1]) << endl;

复杂度:\(O(n^{2})\)

洛谷 P6879 集邮比赛3

题目描述:JOI 2020 Final。环形路上有 n 个邮票和起点 L。到达一个位置可以收集邮票,但必须在截止时间 t[i] 之前到达。求最多能收集多少个邮票。

题目解析:环形破环成链,变为 2n 长度。dp[l][r][c][0] 表示走过区间 [l, r],收集了 c 个邮票,当前在 l 的最早时间;dp[l][r][c][1] 表示当前在 r 的最早时间。转移时区分"在截止时间内收集"和"纯粹路过"。

int n, L;
cin >> n >> L;
vector<int> p(n), t(n);
for (int i = 0; i < n; i++) cin >> p[i];
for (int i = 0; i < n; i++) cin >> t[i];
p.push_back(L); t.push_back(-1);
for (int i = 0; i < n; i++) p.push_back(p[i] + L), t.push_back(t[i]);
const i64 inf = LLONG_MAX / 2;
i64 dp[401][401][401][2];
for (int l = 2 * n; l >= 0; l--) {
    dp[l][l][0][0] = dp[l][l][0][1] = (l == n ? 0 : inf);
    for (int c = 1; c <= n; c++) dp[l][l][c][0] = dp[l][l][c][1] = inf;
    for (int r = l + 1; r <= 2 * n; r++) {
        for (int c = 0; c <= n; c++) {
            dp[l][r][c][0] = inf;
            // 从 l+1 走到 l(路过,不收集 l 处邮票)
            if (dp[l+1][r][c][0] + abs(p[l+1]-p[l]) > t[l])
                dp[l][r][c][0] = min(dp[l][r][c][0], dp[l+1][r][c][0] + abs(p[l+1]-p[l]));
            // 从 l+1 走到 l(在截止时间内到达,收集 l 处邮票)
            if (c && dp[l+1][r][c-1][0] + abs(p[l+1]-p[l]) <= t[l])
                dp[l][r][c][0] = min(dp[l][r][c][0], dp[l+1][r][c-1][0] + abs(p[l+1]-p[l]));
            // 从 r 走到 l(路过 / 收集)
            if (dp[l+1][r][c][1] + abs(p[r]-p[l]) > t[l])
                dp[l][r][c][0] = min(dp[l][r][c][0], dp[l+1][r][c][1] + abs(p[r]-p[l]));
            if (c && dp[l+1][r][c-1][1] + abs(p[r]-p[l]) <= t[l])
                dp[l][r][c][0] = min(dp[l][r][c][0], dp[l+1][r][c-1][1] + abs(p[r]-p[l]));
            // 当前在 r 的转移(对称,走向右端)
            dp[l][r][c][1] = inf;
            if (dp[l][r-1][c][0] + abs(p[r]-p[l]) > t[r])
                dp[l][r][c][1] = min(dp[l][r][c][1], dp[l][r-1][c][0] + abs(p[r]-p[l]));
            if (c && dp[l][r-1][c-1][0] + abs(p[r]-p[l]) <= t[r])
                dp[l][r][c][1] = min(dp[l][r][c][1], dp[l][r-1][c-1][0] + abs(p[r]-p[l]));
            if (dp[l][r-1][c][1] + abs(p[r]-p[r-1]) > t[r])
                dp[l][r][c][1] = min(dp[l][r][c][1], dp[l][r-1][c][1] + abs(p[r]-p[r-1]));
            if (c && dp[l][r-1][c-1][1] + abs(p[r]-p[r-1]) <= t[r])
                dp[l][r][c][1] = min(dp[l][r][c][1], dp[l][r-1][c-1][1] + abs(p[r]-p[r-1]));
        }
    }
}
int ans = 0;
for (int i = 0; i <= n; i++)
    for (int c = ans + 1; c <= n; c++)
        if (dp[i][i+n][c][0] != inf || dp[i][i+n][c][1] != inf)
            ans = max(ans, c);
cout << ans << endl;

复杂度:\(O(n^{3})\)

AT_abc273_f Hammer 2

题目描述:数轴上有 n 面墙和 n 把锤子。锤子可以砸开对应位置的墙。你从原点出发,求能到达位置 X 的最小移动距离。如果不能到达则输出 -1。

题目解析:将人、墙、锤子、起点、终点全部离散化排序。dp[l][r][0] 表示当前在 l 位置,已经访问了 [l, r] 内所有位置的最小移动距离。转移时需判断:如果要访问的位置是"墙"且其对应的"锤子"不在已访问区间内,则不能访问。

int n, x;
cin >> n >> x;
vector<pair<int, int>> a;
for (int i = 1; i <= n; i++) { int y; cin >> y; a.emplace_back(y, i); }   // 锤子
for (int i = 1; i <= n; i++) { int y; cin >> y; a.emplace_back(y, -i); }  // 墙
a.emplace_back(x, 0);   // 终点
a.emplace_back(0, 0);   // 起点
sort(a.begin(), a.end());
vector<int> key(n + 1);
n = a.size();
for (int i = 0; i < n; i++)
    if (a[i].second < 0) key[-a[i].second] = i;  // 记录每面墙对应锤子的位置
const i64 inf = LLONG_MAX / 2;
vector<vector<array<i64, 2>>> dp(n, vector<array<i64, 2>>(n, {inf, inf}));
i64 ans = inf;
for (int l = n - 1; l >= 0; l--) {
    if (a[l].second == 0 && a[l].first == 0) dp[l][l][0] = dp[l][l][1] = 0;
    for (int r = l + 1; r < n; r++) {
        // 走到左端 l:需确保如果 a[l] 是墙,对应锤子在区间内
        if ((a[l].second > 0 && l + 1 <= key[a[l].second] && key[a[l].second] <= r)
            || a[l].second <= 0) {
            dp[l][r][0] = min(
                dp[l+1][r][0] + abs(a[l].first - a[l+1].first),
                dp[l+1][r][1] + abs(a[l].first - a[r].first)
            );
        }
        // 走到右端 r:同理需确保锤子约束
        if ((a[r].second > 0 && l <= key[a[r].second] && key[a[r].second] <= r - 1)
            || a[r].second <= 0) {
            dp[l][r][1] = min(
                dp[l][r-1][0] + abs(a[r].first - a[l].first),
                dp[l][r-1][1] + abs(a[r].first - a[r-1].first)
            );
        }
        if (dp[l][r][0] != inf && a[l].first == x) ans = min(ans, dp[l][r][0]);
        if (dp[l][r][1] != inf && a[r].first == x) ans = min(ans, dp[l][r][1]);
    }
}
if (ans == inf) cout << -1 << endl;
else cout << ans << endl;

复杂度:\(O(n^{2})\)


2.3 划分DP(基于中间划分点枚举)

特征:枚举选哪个划分点 k,将 [i, j] 拆成两个或多个子区间,合并子区间答案。这是区间DP最经典的形式。

洛谷 P1040 加分二叉树

题目描述:给定一棵 n 个节点的二叉树,每个节点有分值。树的加分规则为:子树加分 = 左子树加分 × 右子树加分 + 根节点分值。求最大加分并输出前序遍历。

题目解析:中序遍历为 1..n,枚举根节点 k 作为划分点。dp[l][r] = max(dp[l][k-1] × dp[k+1][r] + a[k])。同时用 opt[l][r] 记录最优根节点以输出方案。

int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
vector<vector<i64>> dp(n + 2, vector<i64>(n + 2, 1));
for (int l = n; l >= 1; l--) {
    dp[l][l] = a[l];
    for (int r = l + 1; r <= n; r++) {
        dp[l][r] = 0;
        for (int k = l; k <= r; k++) {
            i64 v = dp[l][k - 1] * dp[k + 1][r] + a[k];
            if (v > dp[l][r]) dp[l][r] = v;
        }
    }
}
cout << dp[1][n] << endl;

// 输出方案:用 opt[l][r] 记录最优根节点
vector<vector<int>> opt(n + 2, vector<int>(n + 2));
for (int l = n; l >= 1; l--) {
    dp[l][l] = a[l];
    for (int r = l + 1; r <= n; r++) {
        for (int k = l; k <= r; k++) {
            i64 v = dp[l][k - 1] * dp[k + 1][r] + a[k];
            if (v > dp[l][r]) dp[l][r] = v, opt[l][r] = k;
        }
    }
}
auto output = [&](auto &self, int l, int r) -> void {
    if (l > r) return;
    if (l == r) { cout << l << ' '; return; }
    cout << opt[l][r] << ' ';
    self(self, l, opt[l][r] - 1);
    self(self, opt[l][r] + 1, r);
};
output(output, 1, n);

复杂度:\(O(n^{3})\)

洛谷 P4290 玩具取名

题目描述:有四种字母 W, I, N, G。给定若干变换规则(两个字母合成一个字母),问一个字符串最终能否变成 W/I/N/G。

题目解析:dp[l][r] 用状态压缩的位掩码表示区间 [l, r] 能化简为哪些字母(bit 0 对应 W,bit 1 对应 I,等等)。转移:(1) [l, r-1] + str[r] → 新字母;(2) [l+1, r] + str[l] → 新字母;(3) [l, k] + [k+1, r] 合成。

const string pattern = "WING";
vector<int> cnt(4);
for (auto &c : cnt) cin >> c;
vector<int> trans(676);  // 两个字母编码 → 能合成的新字母的 bitmask
auto get_id = [&](char a, char b) { return (a - 'A') * 26 + (b - 'A'); };
for (int i = 0; i < 4; i++) {
    while (cnt[i]--) {
        string s; cin >> s;
        trans[get_id(s[0], s[1])] |= 1 << i;
    }
}
string str; cin >> str;
int n = str.size();
vector<vector<int>> dp(n, vector<int>(n));
for (int l = n - 1; l >= 0; l--) {
    for (int r = l + 1; r < n; r++) {
        if (l + 1 == r) {
            dp[l][r] = trans[get_id(str[l], str[r])];
        } else {
            // 一侧 + 单个字符
            for (int i = 0; i < 4; i++) {
                if (dp[l][r-1] >> i & 1)
                    dp[l][r] |= trans[get_id(pattern[i], str[r])];
                if (dp[l+1][r] >> i & 1)
                    dp[l][r] |= trans[get_id(str[l], pattern[i])];
            }
            // 划分合并
            for (int k = l + 1; k + 1 < r; k++)
                for (int i = 0; i < 4; i++)
                    if (dp[l][k] >> i & 1)
                        for (int j = 0; j < 4; j++)
                            if (dp[k+1][r] >> j & 1)
                                dp[l][r] |= trans[get_id(pattern[i], pattern[j])];
        }
    }
}
if (dp[0][n-1] == 0) cout << "The name is wrong!" << endl;
else {
    for (int i = 0; i < 4; i++)
        if (dp[0][n-1] >> i & 1) cout << pattern[i];
    cout << endl;
}

复杂度:\(O(n^{3} \cdot 4^{2})\)

CF1771D Hossam and (sub-)palindromic tree

题目描述:给定一棵树,每个节点有一个字母。从起点到终点沿最短路径走,途经的字符形成一个字符串。求整棵树中最长的回文子序列的长度。

题目解析:用 DFS 将树上的路径压成类似欧拉序的序列(每个节点在进入和离开子树时各记录一次),然后在序列上做最长回文子序列的区间DP。dp[l][r] = max(dp[l+1][r], dp[l][r-1]),若 s[l]==s[r] 则还可以从 dp[l+1][r-1] + 2 转移。由于每对字符在序列中被算了两次,答案需除以 2。

int T; cin >> T;
while (T--) {
    int n; cin >> n;
    string str; cin >> str;
    vector<vector<int>> g(n);
    for (int i = 1; i < n; i++) {
        int u, v; cin >> u >> v; u--, v--;
        g[u].push_back(v); g[v].push_back(u);
    }
    string s = " ";
    auto dfs = [&](auto &&dfs, int u, int fa) -> void {
        s += str[u];
        for (auto v : g[u]) {
            if (v == fa) continue;
            dfs(dfs, v, u);
            s += str[u];  // 回溯时也记录
        }
    };
    dfs(dfs, 0, -1);
    n = s.size() - 1;
    vector<vector<int>> dp(n + 2, vector<int>(n + 1));
    for (int l = n; l >= 1; l--) {
        for (int r = l; r <= n; r++) {
            dp[l][r] = max(dp[l+1][r], dp[l][r-1]);
            if (s[l] == s[r])
                dp[l][r] = max(dp[l][r], dp[l+1][r-1] + 2);
        }
    }
    cout << dp[1][n] / 2 << endl;
}

复杂度:\(O(n^{2})\)

UVA10304 Optimal Binary Search Tree

题目描述:给定 n 个键的访问频率,构造一棵最优二叉搜索树,使期望搜索代价最小。每次访问的代价为 (深度+1) × 频率。

题目解析:经典的 OBST 问题。dp[l][r] 表示用 [l, r] 构建 OBST 的最小代价,枚举根节点 k。代价为 dp[l][k-1] + dp[k+1][r] + sum(l, r) - a[k]。用 opt[l][r] 记录最优 k,利用四边形不等式优化:opt[l][r-1] ≤ opt[l][r] ≤ opt[l+1][r]

// O(n^3) 基础写法
int n;
while (cin >> n) {
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++) cin >> a[i];
    vector<int> sum(n + 1);
    partial_sum(a.begin(), a.end(), sum.begin());
    vector<vector<int>> dp(n + 2, vector<int>(n + 2));
    for (int l = n; l >= 1; l--) {
        dp[l][l] = 0;
        for (int r = l + 1; r <= n; r++) {
            dp[l][r] = INT_MAX;
            for (int k = l; k <= r; k++)
                dp[l][r] = min(dp[l][r],
                    dp[l][k-1] + dp[k+1][r] + sum[r] - sum[l-1] - a[k]);
        }
    }
    cout << dp[1][n] << endl;
}

// O(n^2) 四边形不等式优化
vector<vector<int>> opt(n + 2, vector<int>(n + 2));
for (int l = n; l >= 1; l--) {
    dp[l][l] = 0; opt[l][l] = l;
    for (int r = l + 1; r <= n; r++) {
        dp[l][r] = INT_MAX;
        for (int k = opt[l][r-1]; k <= opt[l+1][r]; k++) {
            int v = dp[l][k-1] + dp[k+1][r] + sum[r] - sum[l-1] - a[k];
            if (v < dp[l][r]) dp[l][r] = v, opt[l][r] = k;
        }
    }
}

复杂度:基础 \(O(n^{3})\),四边形不等式优化到 \(O(n^{2})\)

洛谷 P1864 二叉查找树

题目描述:NOI 2009。给定 n 个节点,每个节点有值 v、权值 w、访问频率 p。可以修改 v(代价 K × 修改量),也可以修改 w(代价为修改量)。构造一棵 BST 使总代价最小。

题目解析:先按 v 排序(BST 的中序遍历),然后对 w 离散化。dp[limit][l][r] 表示权值下界为 limit 时,用 [l, r] 建树的最小代价。枚举根 k:若 w[k] ≥ limit 则不需要付出额外权值代价;否则需要付出 K。

int n, K;
cin >> n >> K;
struct Node { int v, w, p; };
vector<Node> node(n + 1);
for (int i = 1; i <= n; i++) cin >> node[i].v;
for (int i = 1; i <= n; i++) cin >> node[i].w;
for (int i = 1; i <= n; i++) cin >> node[i].p;
sort(node.begin() + 1, node.end(), [](const Node &a, const Node &b) { return a.v < b.v; });
// w 离散化
vector<int> w(n);
for (int i = 1; i <= n; i++) w[i-1] = node[i].w;
sort(w.begin(), w.end());
w.erase(unique(w.begin(), w.end()), w.end());
for (int i = 1; i <= n; i++) node[i].w = lower_bound(w.begin(), w.end(), node[i].w) - w.begin();
// sump: 访问概率前缀和
vector<int> sump(n + 1);
for (int i = 1; i <= n; i++) sump[i] = sump[i-1] + node[i].p;
auto sum = [&](int l, int r) { return sump[r] - sump[l-1]; };
int dp[71][71][71];
int m = w.size();
for (int limit = m - 1; limit >= 0; limit--) {
    for (int l = n; l >= 1; l--) {
        dp[limit][l][l] = node[l].p + (node[l].w < limit ? K : 0);
        for (int r = l + 1; r <= n; r++) {
            dp[limit][l][r] = INT_MAX;
            for (int k = l; k <= r; k++) {
                if (node[k].w >= limit)
                    dp[limit][l][r] = min(dp[limit][l][r],
                        dp[node[k].w][l][k-1] + dp[node[k].w][k+1][r] + sum(l, r));
                dp[limit][l][r] = min(dp[limit][l][r],
                    dp[limit][l][k-1] + dp[limit][k+1][r] + K + sum(l, r));
            }
        }
    }
}
cout << dp[0][1][n] << endl;

复杂度:\(O(m \cdot n^{3})\),m 为离散化后 w 的取值数。

AT_abc163_e Active Infants

题目描述:n 个小孩排成一行,每人有一个活跃度 a[i]。可以重新排列小孩的顺序。小孩 i 的开心度 = a[i] × |原位置 - 新位置|。求最大总开心度。

题目解析:贪心思路——活跃度大的小孩应该放到两端。按活跃度从小到大排序,依次决定放在剩余区间的左端还是右端。dp[l][r] 表示已经处理了 r-l+1 个小孩(当前最大活跃度的那些),他们被分配到 [l, r] 中某些位置的最大开心度。

int n; cin >> n;
vector<pair<i64, int>> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i].first, a[i].second = i;
sort(a.begin() + 1, a.end());  // 按活跃度从小到大排序
vector<vector<i64>> dp(n + 1, vector<i64>(n + 1));
for (int l = n; l >= 1; l--) {
    dp[l][l] = a[1].first * abs(a[1].second - l);  // 最小的放最远
    for (int r = l + 1; r <= n; r++) {
        int len = r - l + 1;
        dp[l][r] = max(
            dp[l+1][r] + a[len].first * abs(a[len].second - l),  // 放左端
            dp[l][r-1] + a[len].first * abs(a[len].second - r)   // 放右端
        );
    }
}
cout << dp[1][n] << endl;

复杂度:\(O(n^{2})\)

CF2074G Game With Triangles: Season 2

题目描述:环形数组上,每次可以选择三个位置 i<j<k 且 i,k 不相邻,计算 a[i]×a[j]×a[k] 并删除中间元素(或选择跳过)。求最大总分。

题目解析:破环成链(将数组复制一倍) + 区间DP。dp[l][r] 表示区间 [l, r] 的最大得分。转移:(1) 跳过端点;(2) 在中间某点 k 处划分;(3) 选 a[l], a[k], a[r] 构成三角形,消除 [l+1, k-1][k+1, r-1] 的内部后得到三角形分数。

int T; cin >> T;
while (T--) {
    int n; cin >> n;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++) cin >> a[i];
    a.insert(a.end(), a.begin() + 1, a.end());  // 破环成链
    n *= 2;
    vector<vector<i64>> dp(n + 2, vector<i64>(n + 2));
    for (int l = n; l >= 1; l--) {
        for (int r = l; r <= n; r++) {
            dp[l][r] = max(dp[l][r-1], dp[l+1][r]);
            for (int k = l + 1; k < r; k++) {
                dp[l][r] = max(dp[l][r], dp[l][k] + dp[k+1][r]);
                dp[l][r] = max(dp[l][r],
                    dp[l+1][k-1] + dp[k+1][r-1] + a[l]*a[k]*a[r]);
            }
        }
    }
    n /= 2;
    i64 ans = 0;
    for (int i = 1; i <= n; i++)
        ans = max(ans, dp[i][i + n - 1]);
    cout << ans << endl;
}

复杂度:\(O(n^{3})\)


2.4 环形区间DP

特征:序列是环形的。核心技巧——破环成链:将原数组复制一份接到末尾,得到 2n 的数组,区间DP后滑动窗口统计 ans = max/min(dp[i][i+n-1])

洛谷 P1063 能量项链

题目描述:n 颗珠子串成环形项链,每颗有头标记和尾标记。聚合相邻两颗释放能量 = 前珠头 × 前珠尾 × 后珠尾。求最大总能量。

题目解析:破环成链后,dp[l][r] 表示合并区间 [l, r] 的最大能量,枚举中间合并点 k。

int n; cin >> n;
vector<int> a(n);
for (auto &x : a) cin >> x;
a.insert(a.end(), a.begin(), a.end());  // 破环成链
vector<vector<int>> dp(2 * n, vector<int>(2 * n));
for (int l = 2 * n - 1; l >= 0; l--) {
    for (int r = l + 2; r < 2 * n; r++) {
        for (int k = l + 1; k < r; k++)
            dp[l][r] = max(dp[l][r], dp[l][k] + dp[k][r] + a[l] * a[k] * a[r]);
    }
}
int ans = 0;
for (int i = 0; i < n; i++)
    ans = max(ans, dp[i][i + n]);
cout << ans << endl;

复杂度:\(O(n^{3})\)

洛谷 P1043 数字游戏

题目描述:环形数组,分成 m 段,每段之和 mod 10 后相乘。分别求最小乘积和最大乘积。

题目解析:环形破链 + 多段划分DP。dp[l][r][k] 表示区间 [l, r] 分成 k 段的最值。枚举最后一段的划分点。

int n, m;
cin >> n >> m;
vector<int> arr(n + 1);
for (int i = 1; i <= n; i++) cin >> arr[i];
arr.insert(arr.end(), arr.begin() + 1, arr.end());
vector<vector<int>> sum(2 * n + 1, vector<int>(2 * n + 1));
for (int i = 1; i <= 2 * n; i++)
    for (int j = i; j <= 2 * n; j++)
        sum[i][j] = ((sum[i][j-1] + arr[j]) % 10 + 10) % 10;
auto calc = [&](const auto &cmp) {
    vector<vector<vector<i64>>> dp(2 * n + 1,
        vector<vector<i64>>(2 * n + 1, vector<i64>(m + 1, -1)));
    for (int l = 2 * n; l >= 1; l--) {
        for (int r = l; r <= 2 * n; r++) {
            dp[l][r][1] = sum[l][r];
            for (int k = 2; k <= min(r - l + 1, m); k++)
                for (int c1 = 1; c1 < k; c1++)
                    for (int p = l; p < r; p++)
                        if (dp[l][p][c1] != -1 && dp[p+1][r][k-c1] != -1)
                            dp[l][r][k] = dp[l][r][k] == -1
                                ? dp[l][p][c1] * dp[p+1][r][k-c1]
                                : cmp(dp[l][r][k], dp[l][p][c1] * dp[p+1][r][k-c1]);
        }
    }
    i64 ans = dp[1][n][m];
    for (int i = 2; i + n - 1 <= 2 * n; i++)
        ans = cmp(ans, dp[i][i + n - 1][m]);
    return ans;
};
cout << calc([](i64 a, i64 b) { return min(a, b); }) << endl;
cout << calc([](i64 a, i64 b) { return max(a, b); }) << endl;

复杂度:\(O(n^{3} m)\)

洛谷 P4342 Polygon

题目描述:IOI 1998。环形顶点和边交替排列,顶点有权值,边上为 '+' 或 '×'。每次删一条边合并两端点。求最终能得到的最大值及第一步删哪条边。

题目解析:环形破环 + 同时维护最大最小值(乘法涉及负数×负数=正数)。dp[l][r][0] = 最大值,dp[l][r][1] = 最小值。乘法转移需考虑 min×min, min×max, max×min, max×max 四种组合。

int n; cin >> n;
vector<char> op(n);
vector<int> val(n);
for (int i = 0; i < n; i++) cin >> op[i] >> val[i];
op.insert(op.end(), op.begin(), op.end());
val.insert(val.end(), val.begin(), val.end());
vector<vector<array<int, 2>>> dp(2 * n, vector<array<int, 2>>(2 * n));
auto calc = [&](int a, int b, char op) {
    if (op == 't') return a + b;
    return a * b;
};
for (int l = 2 * n - 1; l >= 0; l--) {
    dp[l][l] = {val[l], val[l]};
    for (int r = l + 1; r < 2 * n; r++) {
        dp[l][r] = {INT_MIN, INT_MAX};
        for (int k = l; k < r; k++) {
            for (int d = 0; d < 4; d++) {
                int v = calc(dp[l][k][d & 1], dp[k+1][r][d >> 1 & 1], op[k+1]);
                dp[l][r][0] = max(dp[l][r][0], v);
                dp[l][r][1] = min(dp[l][r][1], v);
            }
        }
    }
}
int ans = INT_MIN;
for (int i = 0; i < n; i++)
    ans = max(ans, dp[i][i + n - 1][0]);
cout << ans << endl;
for (int i = 0; i < n; i++)
    if (dp[i][i + n - 1][0] == ans) cout << i + 1 << ' ';
cout << endl;

复杂度:\(O(n^{3})\)


2.5 二维区间DP

特征:状态定义为矩形区域的四个顶点 dp[x1][x2][y1][y2]。沿行或列切分,每次将矩形切成两个小矩形。

洛谷 P1436 棋盘分割

题目描述:8×8 棋盘,切成 n 块矩形。每块的分数为该块内数字之和的平方。求各块分数之和的最小值(等价于最小化均方差)。

题目解析:dp[l][r][d][u][k] 表示子矩形切成 k 块的最小分数。枚举切第 k-1 刀的位置:竖切或横切,左边/上边保留 k-1 块或右边/下边保留 k-1 块。

int n; cin >> n;
int s[9][9] = {};
for (int i = 1; i <= 8; i++)
    for (int j = 1; j <= 8; j++) {
        cin >> s[i][j];
        s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];  // 二维前缀和
    }
auto calc = [&](int x1, int x2, int y1, int y2) {
    int v = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];
    return v * v;
};
int dp[9][9][9][9][16];
for (int l = 8; l >= 1; l--) {
    for (int r = l; r <= 8; r++) {
        for (int d = 8; d >= 1; d--) {
            for (int u = d; u <= 8; u++) {
                dp[l][r][d][u][1] = calc(l, r, d, u);
                for (int k = 2; k <= n; k++) {
                    dp[l][r][d][u][k] = INT_MAX / 2;
                    for (int i = l; i < r; i++) {  // 竖切
                        dp[l][r][d][u][k] = min(dp[l][r][d][u][k],
                            dp[l][i][d][u][k-1] + calc(i+1, r, d, u));
                        dp[l][r][d][u][k] = min(dp[l][r][d][u][k],
                            dp[i+1][r][d][u][k-1] + calc(l, i, d, u));
                    }
                    for (int i = d; i < u; i++) {  // 横切
                        dp[l][r][d][u][k] = min(dp[l][r][d][u][k],
                            dp[l][r][d][i][k-1] + calc(l, r, i+1, u));
                        dp[l][r][d][u][k] = min(dp[l][r][d][u][k],
                            dp[l][r][i+1][u][k-1] + calc(l, r, d, i));
                    }
                }
            }
        }
    }
}
cout << dp[1][8][1][8][n] << '\n';

复杂度:\(O(8^{4} \cdot n) = O(n)\)(因为棋盘大小固定为 8×8)。


2.6 非完全区间DP

特征:答案并非求 [1, n] 整段区间的最优解,而是由若干个互不重叠的子区间组成。

方法:结合线性DP + 区间DP——先用区间DP算出每个连续段的内部最优解,再用线性DP从前缀中选择不重叠段。

洛谷 P12330 合并石子

题目描述:蓝桥杯 2023 省选。三种颜色的石子排成一行,每堆石子有颜色 c[i] 和数量 v[i]。相邻两堆同色可以合并为新颜色(0→1→2→0),合并代价为两堆数量之和。求最少合并次数和最小总代价。

题目解析:先区间DP:dp[l][r][c] 表示 [l, r] 能否合并成颜色 c 的最小代价。再线性DP:linear[i] 表示前 i 堆的最优解(最少合并次数和最小总代价组成的pair)。

int n; cin >> n;
vector<int> v(n + 1), c(n + 1), sum(n + 1);
for (int i = 1; i <= n; i++) cin >> v[i];
for (int i = 1; i <= n; i++) cin >> c[i];
partial_sum(v.begin(), v.end(), sum.begin());
const int inf = INT_MAX / 3;
vector<vector<array<int, 3>>> dp(n + 1, vector<array<int, 3>>(n + 1));
for (int l = n; l >= 1; l--) {
    dp[l][l] = {inf, inf, inf};
    dp[l][l][c[l]] = 0;
    for (int r = l + 1; r <= n; r++) {
        dp[l][r] = {inf, inf, inf};
        for (int k = l; k < r; k++) {
            for (int col = 0; col < 3; col++) {
                if (dp[l][k][col] == inf || dp[k+1][r][col] == inf) continue;
                int nc = (col + 1) % 3;
                dp[l][r][nc] = min(dp[l][r][nc],
                    dp[l][k][col] + dp[k+1][r][col] + sum[r] - sum[l-1]);
            }
        }
    }
}
vector<pair<int, int>> linear(n + 1, {inf, 0});
linear[0] = {0, 0};
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= i; j++) {
        for (int col = 0; col < 3; col++) {
            if (dp[j][i][col] == inf) continue;
            linear[i] = min(linear[i],
                {linear[j-1].first + 1, linear[j-1].second + dp[j][i][col]});
        }
    }
}
cout << linear[n].first << ' ' << linear[n].second << '\n';

复杂度:区间DP \(O(n^{3})\),线性DP \(O(n^{2})\)

洛谷 P6701 Genotype

题目描述:POI 1997。有若干基因变换规则(两个字母变成一个新字母)。给定字符串,问最少能化简为几个字母(每个字母必须是 'S'),或判断无解。

题目解析:区间DP判断每个区间能否化简为某个字母(用位掩码)。然后线性DP统计最少段数。

int T; cin >> T;
vector<vector<int>> trans(26, vector<int>(26));
while (T--) {
    string s; cin >> s;
    trans[s[1]-'A'][s[2]-'A'] |= 1 << (s[0]-'A');
}
int Q; cin >> Q;
while (Q--) {
    string s; cin >> s;
    int n = s.size();
    vector<vector<int>> dp(n, vector<int>(n));
    for (int l = n - 1; l >= 0; l--) {
        dp[l][l] = 1 << (s[l] - 'a');
        for (int r = l + 1; r < n; r++) {
            for (int k = l; k < r; k++) {
                for (int i = 0; i < 26; i++) {
                    if ((dp[l][k] >> i & 1) == 0) continue;
                    for (int j = 0; j < 26; j++) {
                        if ((dp[k+1][r] >> j & 1) == 0) continue;
                        dp[l][r] |= trans[i][j];
                    }
                }
            }
        }
    }
    vector<int> f(n + 1, INT_MAX / 2);
    f[0] = 0;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++)
            if (dp[j-1][i-1] >> ('S' - 'A') & 1)
                f[i] = min(f[i], f[j-1] + 1);
    if (f[n] == INT_MAX / 2) cout << "NIE" << endl;
    else cout << f[n] << endl;
}

复杂度:区间DP \(O(n^{3} \cdot 26^{2})\),线性DP \(O(n^{2})\)

CF1312E Array Shrinking

题目描述:相邻两个相等的数 a[i] == a[i+1] 可以合并为 a[i]+1。求最终数组的最短长度。

题目解析:dp[val][i] 表示以 i 为起点,能合并出值 val 的最右位置+1。核心递推:如果 dp[val][i] 存在且 dp[val][dp[val][i]] 也存在,则 dp[val+1][i] = dp[val][dp[val][i]](两个 val 合并成 val+1)。最后用线性DP求最短长度。

int n; cin >> n;
vector<int> a(n);
for (auto &x : a) cin >> x;
const int limit = 1024;
vector<vector<int>> dp(limit, vector<int>(n, -1));
for (int i = 1; i < limit; i++) {
    for (int j = n - 1; j >= 0; j--) {
        if (a[j] == i) dp[i][j] = j + 1;
        if (dp[i][j] != -1 && dp[i][j] != n && dp[i][dp[i][j]] != -1)
            dp[i+1][j] = dp[i][dp[i][j]];  // 两个 val 合并成 val+1
    }
}
vector<int> f(n + 1, INT_MAX);
f[0] = 0;
for (int i = 0; i < n; i++)
    for (int j = 0; j < limit; j++)
        if (dp[j][i] != -1)
            f[dp[j][i]] = min(f[dp[j][i]], f[i] + 1);
cout << f[n] << endl;

复杂度:\(O(n \cdot limit)\),limit 最大约 1024。


2.7 统计类区间DP

特征:不求最优值,统计方案数。常见于括号序列计数、树的形态计数等。

洛谷 P10956 金字塔

题目描述:UVA1362。给定一棵树的 DFS 序(进入和离开都记录),求可能的不同树结构数量。

题目解析:dp[l][r] 表示 [l, r] 对应的子树形态方案数。当 s[l] == s[r] 时,枚举第一棵子树的结束位置 k(满足 s[k] == s[r]),方案数为 dp[l][k] × dp[k+1][r-1] 的累加。

string s; cin >> s;
int n = s.size();
const i64 mod = 1e9;
vector<vector<i64>> dp(n, vector<i64>(n));
for (int l = n - 1; l >= 0; l--) {
    dp[l][l] = 1;
    for (int r = l + 1; r < n; r++) {
        if (s[l] != s[r]) continue;
        for (int k = l; k < r; k++)
            if (s[k] == s[r])
                dp[l][r] = (dp[l][r] + dp[l][k] * dp[k+1][r-1]) % mod;
    }
}
cout << dp[0][n-1] << endl;

复杂度:\(O(n^{3})\)

CF149D Coloring Brackets

题目描述:给定一个合法括号序列,给每个括号染红/蓝/无色,要求相邻括号颜色不同,且每对匹配的括号恰有一个有颜色。求方案数。

题目解析:先用栈预匹配括号对。dp[l][r][state] 其中 state 的前 9 种表示封闭括号(A)(cl, cr 分别为 0=无色,1=红,2=蓝),后 9 种表示复合括号。转移:(1) (A) —内层颜色和外层不能冲突;(2) AB —合并相邻两个A。

string str; cin >> str;
int n = str.size();
str = ' ' + str;
// 预匹配括号
vector<int> rightIndex(n + 1, -1);
stack<int> st;
for (int i = 1; i <= n; i++) {
    if (str[i] == '(') st.push(i);
    else rightIndex[st.top()] = i, st.pop();
}
const i64 mod = 1000000007;
vector<vector<array<i64, 18>>> dp(n + 1, vector<array<i64, 18>>(n + 1));
for (int l = n - 1; l >= 1; l--) {
    // 初始化长度为2的括号对
    if (str[l] == '(' && str[l+1] == ')')
        for (auto x : {1, 2, 3, 6})  // (红,蓝):3, (红,无):1, (蓝,红):6, (无,红):2
            dp[l][l+1][x] = dp[l][l+1][x+9] = 1;
    for (int r = l + 3; r <= n; r += 2) {
        if (rightIndex[l] == r) {
            // (A): [l,r]是一对匹配括号
            for (int cl = 0; cl < 3; cl++)
                for (int cr = 0; cr < 3; cr++) {
                    if ((cl && cr) || (cl == 0 && cr == 0)) continue;  // 恰一个有颜色
                    int st = cl * 3 + cr;
                    for (int j = 0; j < 9; j++) {
                        int ccl = j / 3, ccr = j % 3;
                        if (cl == ccl && cl) continue;  // 相邻同色不允许
                        if (cr == ccr && cr) continue;
                        dp[l][r][st] = (dp[l][r][st] + dp[l+1][r-1][j]) % mod;
                        dp[l][r][st+9] = (dp[l][r][st+9] + dp[l+1][r-1][j]) % mod;
                    }
                }
        } else {
            // AB: 合并相邻两个A
            int k = rightIndex[l] + 1;
            if (k == 0 || k > r) continue;
            for (int i = 0; i < 9; i++) {
                int cl = i / 3, cr = i % 3;
                for (int j = 0; j < 9; j++) {
                    int ccl = j / 3, ccr = j % 3;
                    if (cr == ccl && cr) continue;  // 相邻同色
                    dp[l][r][cl * 3 + ccr] = (dp[l][r][cl * 3 + ccr]
                        + dp[l][k-1][i+9] * dp[k][r][j]) % mod;
                }
            }
        }
    }
}
i64 ans = 0;
for (int i = 0; i < 9; i++) ans = (ans + dp[1][n][i]) % mod;
cout << ans << endl;

复杂度:\(O(n^{3})\)

洛谷 P10600 括号序列再战猪猪侠

题目描述:BZOJ4350。给定若干对括号的大小关系约束(第 i 对括号必须比第 j 对大),问有多少种合法的括号嵌套方式。

题目解析:dp[l][r][0] 表示 [l, r] 可再分(如 (A)(B)),dp[l][r][1] 表示不可再分的封闭括号 (A)。转移时用二维前缀和快速检查约束:如果存在比 l 小的括号在 [l+1, r] 中,则 l 不能包含 [l+1, r]

int T; cin >> T;
while (T--) {
    int n, m; cin >> n >> m;
    vector<vector<int>> comp(n+1, vector<int>(n+1));
    bool flag = false;
    while (m--) {
        int a, b; cin >> a >> b;
        comp[a][b] = 1;
        if (comp[b][a]) flag = true;  // 矛盾约束
    }
    if (flag) { cout << "0\n"; continue; }
    // 二维前缀和
    auto sum = comp;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            sum[i][j] += sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1];
    auto has = [&](int x1, int x2, int y1, int y2) {
        if (x1 > x2 || y1 > y2) return false;
        return sum[x2][y2] - sum[x1-1][y2] - sum[x2][y1-1] + sum[x1-1][y1-1] > 0;
    };
    const i64 mod = 998244353;
    vector<vector<array<i64, 2>>> dp(n+1, vector<array<i64, 2>>(n+1));
    for (int l = n; l >= 1; l--) {
        if (!has(l+1, n, l, l)) dp[l][l] = {1, 1};
        for (int r = l + 1; r <= n; r++) {
            // (A): l 必须比 [l+1, r] 中所有人都大
            if (!has(l, l, l+1, r)) {
                dp[l][r][0] = (dp[l][r][0] + dp[l+1][r][0]) % mod;
                dp[l][r][1] = (dp[l][r][1] + dp[l+1][r][0]) % mod;
            }
            // AB: [l,k] 必须比 [k+1, r] 小
            for (int k = l; k < r; k++) {
                if (has(k+1, r, l, k)) continue;
                dp[l][r][0] = (dp[l][r][0] + dp[l][k][1] * dp[k+1][r][0]) % mod;
            }
        }
    }
    cout << dp[1][n][0] << '\n';
}

复杂度:\(O(n^{3})\)

洛谷 P7914 括号序列

题目描述:CSP-S 2021。给定带 ? 的括号序列和星号长度限制 k,求合法的超级括号序列方案数。规则包括:()、(S)、(AS)、(SA)、A+S+A。

题目解析:用5个状态:dp[l][r][0] = S(连续星号),dp[l][r][1] = A(完整序列),dp[l][r][2] = AS,dp[l][r][3] = SA,dp[l][r][4] = 封闭括号(A)。转移时按规则拼接。

int n, k; string str;
cin >> n >> k >> str;
const i64 mod = 1e9 + 7;
vector<vector<array<i64, 5>>> dp(n, vector<array<i64, 5>>(n));
for (int l = n - 1; l >= 0; l--) {
    for (int r = l; r < n; r++) {
        auto &res = dp[l][r];
        fill(res.begin(), res.end(), 0);
        int len = r - l + 1;
        // S: 全为 * 或 ?,长度不超过 k
        if (len <= k && all_of(str.begin()+l, str.begin()+r+1,
            [](char c) { return c == '*' || c == '?'; })) res[0] = 1;
        // () 或 (A)/(AS)/(SA)/(S)
        if (l != r && (str[l] == '(' || str[l] == '?') && (str[r] == ')' || str[r] == '?')) {
            if (l + 1 == r) res[1] = res[4] = 1;
            else for (int st = 0; st < 4; st++) {
                res[1] = (res[1] + dp[l+1][r-1][st]) % mod;
                res[4] = (res[4] + dp[l+1][r-1][st]) % mod;
            }
        }
        // 拼接
        for (int k = l; k < r; k++) {
            res[1] = (res[1] + dp[l][k][4] * dp[k+1][r][1]) % mod;  // A = (A') + A
            res[1] = (res[1] + dp[l][k][4] * dp[k+1][r][3]) % mod;  // A = (A') + SA
            res[2] = (res[2] + dp[l][k][1] * dp[k+1][r][0]) % mod;  // AS = A + S
            res[3] = (res[3] + dp[l][k][0] * dp[k+1][r][1]) % mod;  // SA = S + A
        }
    }
}
cout << dp[0][n-1][1] << '\n';

复杂度:\(O(n^{3})\)

AT_abc252_g Pre-Order

题目描述:给定一棵有根树的前序遍历序列,每个节点的孩子按编号排序遍历。求可能的树结构数量。

题目解析:前序遍历的第一个元素一定是根。dp[l][r][0] 表示 [l, r] 构成封闭的树,dp[l][r][1] 表示可继续添加兄弟。枚举第二棵子树的起点 k,需满足 a[k] > a[l]

int n; cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
const i64 mod = 998244353;
vector<vector<array<i64, 2>>> dp(n + 1, vector<array<i64, 2>>(n + 1));
for (int l = n; l >= 1; l--) {
    dp[l][l][0] = dp[l][l][1] = 1;
    for (int r = l + 1; r <= n; r++) {
        dp[l][r][0] = dp[l][r][1] = dp[l+1][r][1];
        for (int k = l + 1; k <= r; k++)
            if (a[k] > a[l])
                dp[l][r][1] = (dp[l][r][1]
                    + dp[l][k-1][0] * dp[k][r][1]) % mod;
    }
}
cout << dp[1][n][0] << endl;

复杂度:\(O(n^{3})\)


2.8 线段类区间DP

特征:以线段/区间覆盖为背景,先对坐标离散化,然后对离散化后的位置做区间DP。

洛谷 P4766 Outer space invaders

题目描述:CERC 2014。有 n 个外星人,每个在时刻 [a[i], b[i]] 出现,距离为 d[i]。你可以在任意时刻发动攻击,攻击半径为 R,消灭距离 ≤ R 的所有外星人。每次攻击代价为 R。求消灭所有外星人的最小代价。

题目解析:离散化坐标,区间DP以"最远的外星人"作为决策点。dp[l][r] 表示消灭完全在 [l, r] 内的外星人的最小代价。找到区间内距离最大的外星人,枚举在它的时间范围内发动攻击的时刻 k。

int T; cin >> T;
while (T--) {
    int n; cin >> n;
    vector<int> a(n), b(n), d(n);
    for (int i = 0; i < n; i++) cin >> a[i] >> b[i] >> d[i];
    vector<int> p = a;
    p.push_back(-1);
    p.insert(p.end(), b.begin(), b.end());
    sort(p.begin(), p.end());
    p.erase(unique(p.begin(), p.end()), p.end());
    for (int i = 0; i < n; i++) {
        a[i] = lower_bound(p.begin(), p.end(), a[i]) - p.begin();
        b[i] = lower_bound(p.begin(), p.end(), b[i]) - p.begin();
    }
    int P = p.size();
    vector<vector<int>> dp(P + 2, vector<int>(P + 2));
    for (int l = P; l >= 1; l--) {
        for (int r = l; r <= P; r++) {
            dp[l][r] = INT_MAX;
            int max_d = 0, max_id = 0;
            for (int i = 0; i < n; i++) {
                if (a[i] < l || b[i] > r) continue;
                if (d[i] > max_d) max_d = d[i], max_id = i;
            }
            for (int k = a[max_id]; k <= b[max_id]; k++)
                dp[l][r] = min(dp[l][r], dp[l][k-1] + dp[k+1][r] + max_d);
        }
    }
    cout << dp[1][P] << endl;
}

复杂度:\(O(P^{3})\),P 为离散化后坐标数。

洛谷 P5851 Greedy Pie Eaters P

题目描述:USACO 2019 Dec。有 n 个派排成一行,m 头奶牛。第 i 头奶牛可以吃 [a[i], b[i]] 区间内的任意一个派,产生 w[i] 的满意度。每个派只能被一头牛吃。求最大总满意度。

题目解析:max_w[l][r][k] 表示在区间 [l, r] 内,第 k 个派能被吃的最大满意度。dp[l][r] 表示 [l, r] 的最大总满意度,枚举最后一个被吃的派 k。

int n, m; cin >> n >> m;
int max_w[301][301][301] = {};
for (int i = 0; i < m; i++) {
    int w, a, b; cin >> w >> a >> b;
    for (int j = a; j <= b; j++) max_w[a][b][j] = w;
}
// 扩展:大区间的权重包含小区间
for (int l = n; l >= 1; l--)
    for (int r = l; r <= n; r++)
        for (int k = l; k <= r; k++) {
            if (k != l) max_w[l][r][k] = max(max_w[l][r][k], max_w[l+1][r][k]);
            if (k != r) max_w[l][r][k] = max(max_w[l][r][k], max_w[l][r-1][k]);
        }
vector<vector<int>> dp(n + 2, vector<int>(n + 2));
for (int l = n; l >= 1; l--)
    for (int r = l; r <= n; r++)
        for (int k = l; k <= r; k++)
            dp[l][r] = max(dp[l][r],
                dp[l][k-1] + dp[k+1][r] + max_w[l][r][k]);
cout << dp[1][n] << endl;

复杂度:\(O(n^{3})\)

AT_arc183_c Not Argmax

题目描述:给定 m 条约束 argmax(l, r) ≠ x,即区间 [l, r] 的最大值不能位于位置 x。求满足所有约束的 1..n 排列个数。

题目解析:dp[l][r] 表示 [l, r] 的合法方案数。枚举区间最大值的位置 k。用 fbd[l][r][x] 标记哪些位置不能是最大值。方案数 = C(r-l, k-l) × dp[l][k-1] × dp[k+1][r](组合数选哪些值分给左子树)。

int n, m; cin >> n >> m;
bool fbd[502][501][501] = {};
while (m--) {
    int l, r, x; cin >> l >> r >> x;
    fbd[l][r][x] = 1;
}
const i64 mod = 998244353;
// 预处理组合数
vector<vector<i64>> C(n+1, vector<i64>(n+1));
for (int i = 0; i <= n; i++)
    for (int j = 0; j <= i; j++)
        if (j == 0 || i == j) C[i][j] = 1;
        else C[i][j] = (C[i-1][j-1] + C[i-1][j]) % mod;
vector<vector<i64>> dp(n+2, vector<i64>(n+1, 1));
for (int l = n; l >= 1; l--) {
    for (int r = l; r <= n; r++) {
        for (int x = l; x <= r; x++)
            fbd[l][r][x] |= fbd[l][r-1][x] | fbd[l+1][r][x];
        dp[l][r] = 0;
        for (int k = l; k <= r; k++) {
            if (fbd[l][r][k]) continue;
            dp[l][r] = (dp[l][r]
                + dp[l][k-1] * dp[k+1][r] % mod * C[r-l][k-l]) % mod;
        }
    }
}
cout << dp[1][n] << endl;

复杂度:\(O(n^{3})\)

洛谷 P3592 MYJ

题目描述:POI 2015。有 n 个洗车位,m 辆车。第 i 辆车在 [a[i], b[i]] 范围内洗车,洗车价格为 c[i]。如果最终价格 > c[i],该车就不洗了。最大化总收入,输出每个位置的价格。

题目解析:离散化价格。dp[l][r][p] 表示区间 [l, r] 内价格 ≥ 价格等级 p 的最大收入。枚举最小值位置 k(定价为 price[p])。用 cnt[k][p] 统计当 k 是最小价格位置时有多少车会在 k 洗车(这些车的区间覆盖 k 且 c ≥ price[p])。输出方案:opt[l][r][p] 记录决策。

int n, m; cin >> n >> m;
vector<int> a(m), b(m), c(m), d(m);
for (int i = 0; i < m; i++) cin >> a[i] >> b[i] >> c[i], d[i] = c[i];
sort(d.begin(), d.end());
d.erase(unique(d.begin(), d.end()), d.end());
for (auto &x : c) x = lower_bound(d.begin(), d.end(), x) - d.begin();
int P = d.size();
int dp[52][52][4001] = {}, opt[52][52][4001] = {};
vector<vector<int>> cnt(n+1, vector<int>(P));
for (int l = n; l >= 1; l--) {
    for (int r = l; r <= n; r++) {
        // 统计 cnt[k][p]
        for (int k = l; k <= r; k++) fill(cnt[k].begin(), cnt[k].end(), 0);
        for (int i = 0; i < m; i++) {
            if (a[i] < l || b[i] > r) continue;
            for (int k = a[i]; k <= b[i]; k++) cnt[k][c[i]]++;
        }
        for (int k = l; k <= r; k++)
            for (int p = P-2; p >= 0; p--)
                cnt[k][p] += cnt[k][p+1];
        for (int p = P-1; p >= 0; p--) {
            for (int k = l; k <= r; k++) {
                int v = dp[l][k-1][p] + dp[k+1][r][p] + cnt[k][p] * d[p];
                if (v > dp[l][r][p]) dp[l][r][p] = v, opt[l][r][p] = k;
            }
            if (dp[l][r][p+1] > dp[l][r][p])
                dp[l][r][p] = dp[l][r][p+1], opt[l][r][p] = -1;
        }
    }
}
cout << dp[1][n][0] << endl;
// 输出方案
vector<int> ans(n+1);
auto dfs = [&](auto &self, int l, int r, int p) {
    if (opt[l][r][p] == 0) return;
    if (opt[l][r][p] == -1) { self(self, l, r, p+1); return; }
    int k = opt[l][r][p];
    ans[k] = d[p];
    self(self, l, k-1, p);
    self(self, k+1, r, p);
};
dfs(dfs, 1, n, 0);
for (int i = 1; i <= n; i++)
    cout << (ans[i] ? ans[i] : d.back()) << ' ';
cout << endl;

复杂度:\(O(n^{3} \cdot P)\),P 为离散化后价格等级数。


2.9 祖玛类区间DP(消除类)

特征:消除游戏背景,核心是相邻相同颜色/元素可以合并或消除。这是区间DP中最复杂的一类。通常需要额外维度记录"相同元素连续多少个",复杂度可达 \(O(n^{4})\)

核心模式f[l][r][c] 记录前缀信息(与 a[l] 同色的元素个数),核心转移为 f[l][r][c] ← f[l][k][c-1] + dp[k+1][r-1](满足 a[l]a[k]a[r]),然后 dp[l][r] ← f[l][r][c] + cost(c)

洛谷 P4170 涂色

题目描述:CQOI 2007。一块木板初始无色,每次可以选一个区间涂成同一种颜色(覆盖)。求涂成目标颜色的最少次数。

题目解析:dp[l][r] 表示涂完 [l, r] 的最少次数。关键优化:如果 s[l] == s[r],则涂 [l, r-1][l+1, r] 时顺便覆盖另一端。另外,同色中间点 k 可帮助合并。

string s; cin >> s;
int n = s.size();
s = ' ' + s;
const int inf = INT_MAX / 2;
vector<vector<int>> dp(n+1, vector<int>(n+1));
for (int l = n; l >= 1; l--) {
    for (int r = l; r <= n; r++) {
        // 全部同色:一次涂完
        if (all_of(s.begin()+l, s.begin()+r+1, [&](char c) { return c == s[l]; })) {
            dp[l][r] = 1; continue;
        }
        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]);
        if (s[l] == s[r])
            for (int k = l; k < r; k++)
                if (s[k] == s[l])
                    dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r-1]);
    }
}
cout << dp[1][n] << endl;

复杂度:\(O(n^{3})\)

CF607B Zuma

题目描述:给定序列,每次可以删除一个回文子串(连续),求删完整个序列的最少操作次数。

题目解析:dp[l][r] 表示删除 [l, r] 的最少次数。如果 [l, r] 是回文则一次删完。否则可划分,也可利用 a[l]==a[r]dp[l+1][r-1] 一起删。

int n; cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
const int inf = 1e9;
auto is_palindrome = [&](int l, int r) {
    while (l < r) { if (a[l] != a[r]) return false; l++, r--; }
    return true;
};
vector<vector<int>> dp(n+2, vector<int>(n+2));
for (int l = n; l >= 1; l--) {
    for (int r = l; r <= n; r++) {
        dp[l][r] = inf;
        if (is_palindrome(l, r)) dp[l][r] = 1;
        for (int k = l; k < r; k++)
            dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r]);
        if (a[l] == a[r] && l + 1 <= r - 1)
            dp[l][r] = min(dp[l][r], dp[l+1][r-1]);
    }
}
cout << dp[1][n] << endl;

复杂度:\(O(n^{3})\)

AT_abc217_f Make Pair

题目描述:2n 个人排成一行,有 m 对朋友关系。每次可将相邻且是朋友的一对人删除。求删除所有人的方案数。

题目解析:dp[l][r][0] 表示 [l, r] 可删完的方案数(可再分为多段),dp[l][r][1] 表示不可再分的封闭块 (A)。转移:(1) 两端是朋友则自成封闭块;(2) 划分为前一段封闭+后一段可删。

int n, m; cin >> n >> m; n *= 2;
vector<vector<int>> friends(n+1, vector<int>(n+1));
for (int i = 1; i <= m; i++) {
    int u, v; cin >> u >> v;
    friends[u][v] = friends[v][u] = 1;
}
vector<vector<i64>> C(n+1, vector<i64>(n+1));
for (int i = 0; i <= n; i++)
    for (int j = 0; j <= i; j++)
        if (j == 0 || j == i) C[i][j] = 1;
        else C[i][j] = (C[i-1][j-1] + C[i-1][j]) % mod;
const i64 mod = 998244353;
vector<vector<array<i64, 2>>> dp(n+1, vector<array<i64, 2>>(n+1));
for (int l = n; l >= 1; l--) {
    if (l+1 <= n && friends[l][l+1]) dp[l][l+1][0] = dp[l][l+1][1] = 1;
    for (int r = l+3; r <= n; r += 2) {
        for (int k = l+1; k < r; k += 2) {
            int pair = (r-l+1)/2, left = (k-l+1)/2;
            dp[l][r][0] = (dp[l][r][0]
                + dp[l][k][1] * dp[k+1][r][0] % mod * C[pair][left]) % mod;
        }
        if (friends[l][r]) {
            dp[l][r][0] = (dp[l][r][0] + dp[l+1][r-1][0]) % mod;
            dp[l][r][1] = (dp[l][r][1] + dp[l+1][r-1][0]) % mod;
        }
    }
}
cout << dp[1][n][0] << endl;

复杂度:\(O(n^{3})\)

AT_abc325_g offence

题目描述:字符串由小写字母组成,可进行如下操作:删除子串 "of",然后再删除接下来的 K 个任意字符。求最终字符串的最短长度。

题目解析:dp[l][r] 表示 [l, r] 能删除到的最短长度。三种转移:(1) 跳过一端;(2) 划分合并;(3) 找到 "of" 子串进行删除。

string s; int K;
cin >> s >> K;
int n = s.size();
s = " " + s;
vector<vector<int>> dp(n+2, vector<int>(n+2));
for (int l = n; l >= 1; l--) {
    dp[l][l] = 1;
    for (int r = l+1; r <= n; r++) {
        dp[l][r] = min(dp[l+1][r], dp[l][r-1]) + 1;
        for (int k = l; k < r; k++)
            dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r]);
        if (s[l] == 'o')
            for (int k = l+1; k <= r; k++)
                if (s[k] == 'f' && dp[l+1][k-1] == 0)
                    dp[l][r] = min(dp[l][r], max(0, dp[k+1][r] - K));
    }
}
cout << dp[1][n] << endl;

复杂度:\(O(n^{3})\)

AT_abc400_f Happy Birthday! 3

题目描述:环形蛋糕,每块有颜色 c[i] 和消除代价 x[c]。消除规则:每次选择相邻且颜色相同的两块,代价为 x[c]。求将蛋糕完全消除的最小代价。

题目解析:环形破环成链 + 祖玛类消除。dp[l][r] 表示消除 [l, r] 的最小代价。

int n; cin >> n;
vector<int> c(n+1), x(n+1);
for (int i = 1; i <= n; i++) cin >> c[i];
for (int i = 1; i <= n; i++) cin >> x[i];
c.insert(c.end(), c.begin()+1, c.end());  // 破环成链
n *= 2;
vector<vector<i64>> dp(n+2, vector<i64>(n+2));
for (int l = n; l >= 1; l--) {
    dp[l][l] = 1 + x[c[l]];
    for (int r = l+1; r <= n; r++) {
        dp[l][r] = LLONG_MAX / 2;
        for (int k = l; k < r; k++)
            dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r]);
        if (c[l] == c[r])
            for (int k = l; k < r; k++)
                if (c[k] == c[l])
                    dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r-1] + r - k);
    }
}
n /= 2;
i64 ans = LLONG_MAX;
for (int i = 1; i <= n; i++)
    ans = min(ans, dp[i][i+n-1]);
cout << ans << endl;

复杂度:\(O(n^{3})\)

AT_agc050_b Three Coins

题目描述:一排 n 个格子,每个格子有一个硬币。可以进行操作:选择相邻三个位置 i<j<k(j=k-1 处必须有硬币),将三个位置状态翻转。求最多能有多少硬币。

题目解析:操作实质是三个一组的翻转。当区间长度模 3 余 0 时,可以从内部消除并翻转两端。

int n; cin >> n;
vector<int> a(n+1);
for (int i = 1; i <= n; i++) cin >> a[i];
vector<vector<i64>> dp(n+2, vector<i64>(n+2));
for (int l = n; l >= 1; l--) {
    for (int r = l; r <= n; r++) {
        for (int k = l; k < r; k++)
            dp[l][r] = max(dp[l][r], dp[l][k] + dp[k+1][r]);
        if ((r - l + 1) % 3 == 0)
            for (int k = l+1; k < r; k += 3)
                dp[l][r] = max(dp[l][r],
                    dp[l+1][k-1] + dp[k+1][r-1] + a[l] + a[k] + a[r]);
    }
}
cout << dp[1][n] << endl;

复杂度:\(O(n^{3})\)

洛谷 P5189 ZUMA

题目描述:COCI 2009/2010。祖玛游戏:每次可插入一个珠子,当同色连续长度 ≥ K 时自动消除。求插入最少珠子数。

题目解析:dp[l][r] 表示消除 [l, r] 最少需插入的珠子数。f[l][r][c] 表示消除 [l, r] 后剩下 c 个与 a[l] 同色珠子(作为前缀,c<K)的最小代价。

int n, K; cin >> n >> K;
vector<int> a(n+1);
for (int i = 1; i <= n; i++) cin >> a[i];
const int inf = INT_MAX / 2;
vector<vector<int>> dp(n+1, vector<int>(n+1));
vector<vector<vector<int>>> f(n+1, vector<vector<int>>(n+1, vector<int>(n+1, inf)));
for (int l = n; l >= 1; l--) {
    f[l][l][1] = 0;
    for (int r = l; r <= n; r++) {
        int len = r - l + 1;
        // 全部同色:检查天然可消除
        if (all_of(a.begin()+l, a.begin()+r+1, [&](int x) { return x == a[l]; }))
            dp[l][r] = max(0, K - len);
        else dp[l][r] = INT_MAX;
        // 划分合并
        for (int i = l; i < r; i++)
            dp[l][r] = min(dp[l][r], dp[l][i] + dp[i+1][r]);
        // 同色前缀扩展
        if (a[l] == a[r]) {
            for (int k = l; k < r; k++)
                if (a[k] == a[l])
                    for (int c = 2; c <= len; c++)
                        f[l][r][c] = min(f[l][r][c], f[l][k][c-1] + dp[k+1][r-1]);
            for (int c = 1; c <= len; c++)
                dp[l][r] = min(dp[l][r], f[l][r][c] + max(0, K - c));
        }
    }
}
cout << dp[1][n] << endl;

复杂度:\(O(n^{4})\)

洛谷 P5336 成绩单

题目描述:THUSC 2016。n 次考试成绩,每次可取走连续若干张成绩单,代价为 a + b×(max-min)^2。求取走全部的最小代价。

题目解析:dp[l][r] 表示取走 [l, r] 的最小代价。f[l][r][min][max] 表示 [l, r] 中取走一部分后,剩余成绩单的最小值和最大值分别为 min 和 max(离散化后的下标)时的最小代价。将 w 离散化以减小状态。

int n, a, b; cin >> n >> a >> b;
vector<int> w(n+1);
for (int i = 1; i <= n; i++) cin >> w[i];
auto ww = w;
sort(ww.begin(), ww.end());
ww.erase(unique(ww.begin(), ww.end()), ww.end());
for (int i = 1; i <= n; i++)
    w[i] = lower_bound(ww.begin(), ww.end(), w[i]) - ww.begin();
int m = ww.size();
const int inf = INT_MAX / 2;
vector<vector<int>> dp(n+2, vector<int>(n+1));
int f[52][51][51][51];
for (int l = n; l >= 1; l--) {
    for (int r = l; r <= n; r++) {
        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]);
        memset(f[l][r], 0x7f, sizeof(f[l][r]));
        if (l == r) f[l][r][w[l]][w[l]] = a;
        for (int k = l; k < r; k++) {
            for (int i = 1; i < m; i++) {
                for (int j = 1; j < m; j++) {
                    auto &v = f[l][r][min(i, w[r])][max(j, w[r])];
                    v = min(v, f[l][k][i][j] + dp[k+1][r-1]);
                }
            }
        }
        for (int i = 1; i < m; i++)
            for (int j = 1; j < m; j++)
                dp[l][r] = min(dp[l][r],
                    f[l][r][i][j] + a + b * (ww[i]-ww[j]) * (ww[i]-ww[j]));
    }
}
cout << dp[1][n] << endl;

复杂度:\(O(n^{3} \cdot m^{2})\),m 为离散化后 w 的取值数。

CF1107E Vasya and Binary String

题目描述:01 串,消除连续相同字符。消除长度为 len 的连续段的得分为 w[len]。求最大得分。

题目解析:和 UVA10559 类似。dp[l][r] 表示删除 [l, r] 的最大得分。f[l][r][c] 表示 [l, r] 处理后留下 c 个与 s[l] 相同字符(作为前缀)的最大得分。

int n; string str;
cin >> n >> str;
str.insert(str.begin(), ' ');
vector<int> w(n+1);
for (int i = 1; i <= n; i++) cin >> w[i];
vector<vector<i64>> dp(n+1, vector<i64>(n+1));
i64 f[101][101][101];
for (int l = n; l >= 1; l--) {
    for (int r = l; r <= n; r++) {
        for (int k = l; k < r; k++)
            dp[l][r] = max(dp[l][r], dp[l][k] + dp[k+1][r]);
        memset(f[l][r], 0x80, sizeof(f[l][r]));
        if (str[l] == str[r]) {
            if (l == r) f[l][r][1] = 0;
            for (int k = l; k < r; k++)
                if (str[k] == str[l])
                    for (int i = 2; i <= n; i++)
                        f[l][r][i] = max(f[l][r][i], f[l][k][i-1] + dp[k+1][r-1]);
            for (int i = 1; i <= n; i++)
                dp[l][r] = max(dp[l][r], f[l][r][i] + w[i]);
        }
    }
}
cout << dp[1][n] << endl;

复杂度:\(O(n^{4})\)

UVA10559 方块消除 Blocks

题目描述:经典消除游戏。点击一个颜色块,所有相邻同色块被消除,得分 = 消除个数的平方。消除后左右合并。求最大总得分。

题目解析:和 CF1107E 几乎一样,只是 w[len] = len^2。dp[l][r] 表示删除 [l, r] 的最大得分。f[l][r][c] 表示处理后留下 c 个与 a[l] 同色字符的前缀的最大得分。

int T; cin >> T;
for (int cas = 1; cas <= T; cas++) {
    int n; cin >> n;
    vector<int> a(n+1);
    for (int i = 1; i <= n; i++) cin >> a[i];
    vector<vector<int>> dp(n+1, vector<int>(n+1));
    vector<vector<vector<int>>> f(n+1, vector<vector<int>>(n+1, vector<int>(n+1, INT_MIN/2)));
    for (int l = n; l >= 1; l--) {
        dp[l][l] = 1; f[l][l][1] = 0;
        for (int r = l + 1; r <= n; r++) {
            for (int k = l; k < r; k++)
                dp[l][r] = max(dp[l][r], dp[l][k] + dp[k+1][r]);
            if (a[l] == a[r]) {
                for (int k = l; k < r; k++)
                    if (a[k] == a[l])
                        for (int c = 2; c <= r-l+1; c++)
                            f[l][r][c] = max(f[l][r][c], f[l][k][c-1] + dp[k+1][r-1]);
                for (int c = 2; c <= r-l+1; c++)
                    dp[l][r] = max(dp[l][r], f[l][r][c] + c * c);
            }
        }
    }
    cout << "Case " << cas << ": " << dp[1][n] << endl;
}

复杂度:\(O(n^{4})\)

洛谷 P2145 祖玛

题目描述:JSOI 2007。经典祖玛:≥3 个同色自动消失。给定初始序列,问最少插入珠子数。黑题。

题目解析:先压缩连续同色为 (color, cnt) 对。dp[l][r] 表示消除压缩后 [l, r] 的最小插入数,f[l][r][c] 表示消除后剩余 c (c<3) 个与 color[l] 同色珠子的最小代价。注意 cnt[r] 是否等于 1 对状态转移的影响:若 cnt[r]==1,前缀扩展直接 c+1;若 cnt[r]>1 且 c<2,跳跃到 3(因为两个连续珠子会合并)。

// 压缩:连续同色 → (color, cnt)
vector<int> color(1), cnt(1);
for (int i = 1; i <= n; i++)
    if (i == 1 || a[i] != a[i-1]) color.push_back(a[i]), cnt.push_back(1);
    else cnt.back()++;
n = color.size() - 1;
const int inf = INT_MAX / 2;
vector<vector<int>> dp(n+1, vector<int>(n+1));
vector<vector<array<int, 4>>> f(n+1, vector<array<int, 4>>(n+1, {inf,inf,inf,inf}));
for (int l = n; l >= 1; l--) {
    f[l][l][cnt[l] != 1] = 0;  // cnt[l]==1→剩余1个色,cnt[l]>1→剩余0个色
    dp[l][l] = max(1, 3 - cnt[l]);
    for (int r = l+1; r <= n; r++) {
        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]);
        if (color[l] == color[r])
            for (int k = l; k < r; k++)
                if (color[k] == color[l])
                    for (int c = 0; c <= 2; c++) {
                        if (cnt[r] == 1)
                            f[l][r][c+1] = min(f[l][r][c+1], f[l][k][c] + dp[k+1][r-1]);
                        else if (c != 2)
                            f[l][r][3] = min(f[l][r][3], f[l][k][c] + dp[k+1][r-1]);
                    }
        for (int c = 0; c <= 3; c++)
            dp[l][r] = min(dp[l][r], f[l][r][c] + max(2 - c, 0));
    }
}
cout << dp[1][n] << endl;

复杂度:\(O(n^{3})\)(颜色压缩后 n 变小)。

洛谷 P1389 一个关于序列的游戏

题目描述:给定序列,每次可以删除一个区间,要求该区间内的值是"先递增再递减"的形状,并获得对应长度权值的分数。求最大总得分。

题目解析:dp[l][r] 表示删除 [l, r] 的最大得分。用 inc[l][r]dec[l][r] 分别记录内部消除后只剩下递增/递减序列的最大收益。对于每个可能的山顶位置 k,合并 inc[l][k] + dec[k][r]。最后非完全线性DP。

int n; cin >> n;
vector<int> w(n+1), a(n+1);
for (int i = 1; i <= n; i++) cin >> w[i];
for (int i = 1; i <= n; i++) cin >> a[i];
const int inf = INT_MIN / 2;
vector<vector<int>> dp(n+1, vector<int>(n+1));
auto inc = dp, dec = dp;
for (int l = n; l >= 1; l--) {
    dp[l][l] = w[1]; inc[l][l] = dec[l][l] = 0;
    for (int r = l+1; r <= n; r++) {
        dp[l][r] = inc[l][r] = dec[l][r] = inf;
        for (int k = l; k < r; k++)
            dp[l][r] = max(dp[l][r], dp[l][k] + dp[k+1][r]);
        for (int k = l; k < r; k++) {
            if (inc[l][k] != inf && a[k]+1 == a[r])
                inc[l][r] = max(inc[l][r], inc[l][k] + dp[k+1][r-1]);
            if (dec[l][k] != inf && a[k]-1 == a[r])
                dec[l][r] = max(dec[l][r], dec[l][k] + dp[k+1][r-1]);
        }
        for (int k = l; k <= r; k++) {
            if (inc[l][k] == inf || dec[k][r] == inf) continue;
            int remainLen = a[k]-a[l] + a[k]-a[r] + 1;
            dp[l][r] = max(dp[l][r], w[remainLen] + inc[l][k] + dec[k][r]);
        }
    }
}
vector<int> linear(n+1);
for (int i = 1; i <= n; i++) {
    linear[i] = max(0, linear[i-1]);
    for (int j = 1; j <= i; j++)
        linear[i] = max(linear[i], linear[j-1] + dp[j][i]);
}
cout << linear[n] << '\n';

复杂度:区间DP \(O(n^{3})\),线性DP \(O(n^{2})\)


2.10 特殊划分

特征:字符串压缩/折叠、区间合并等特殊规则,灵活程度高。

洛谷 P2470 压缩

题目描述:SCOI 2007。压缩字符串:用 M 标记重复开始,用 R 标记重复前面的内容。求压缩后的最短长度。

题目解析:comp[l][r] 表示 [l, r] 压缩后的最短长度。如果 [l, r] 可以由前半段重复两次得到,则用 M+R 压缩。最后线性DP合并不重叠段。

string str; cin >> str;
int n = str.size();
vector<vector<int>> comp(n, vector<int>(n, INT_MAX));
for (int l = 0; l < n; l++) {
    for (int r = l; r < n; r++) {
        for (int k = l + 1; k <= r; k += 2) {
            int len = k - l + 1;
            int half = len / 2;
            bool same = true;
            for (int i = 0; i < half; i++)
                if (str[l+i] != str[l+half+i]) { same = false; break; }
            if (!same) continue;
            comp[l][r] = min(comp[l][r], min(half, comp[l][l+half-1]) + 1 + r - k);
        }
    }
}
vector<int> f(n);
for (int i = 0; i < n; i++) {
    f[i] = min(i + 1, comp[0][i]);
    for (int j = 1; j <= i; j++)
        if (comp[j][i] != INT_MAX)
            f[i] = min(f[i], f[j-1] + comp[j][i] + 1);
}
cout << f[n-1] << endl;

复杂度:\(O(n^{3})\)

洛谷 P4302 字符串折叠

题目描述:SCOI 2003。将重复子串折叠成 数字(内容) 的形式。求最短折叠后长度。

题目解析:dp[l][r] 表示 [l, r] 折叠后的最短长度。检查每个可能的循环节长度 len,若 [l, r][l, l+len-1] 重复组成,则可折叠。

string str;
while (cin >> str) {
    int n = str.size();
    const int inf = INT_MAX / 2;
    vector<vector<int>> dp(n, vector<int>(n, inf));
    for (int l = n-1; l >= 0; l--) {
        dp[l][l] = 1;
        for (int r = l+1; r < n; r++) {
            for (int k = l; k < r; k++)  // 划分
                dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r]);
            for (int len = 1; len <= (r-l+1)/2; len++) {
                if ((r-l+1) % len) continue;
                bool ok = true;
                for (int i = l+len; i+len-1 <= r; i += len)
                    for (int j = 0; j < len; j++)
                        if (str[l+j] != str[i+j]) { ok = false; break; }
                if (!ok) continue;
                int rep = (r-l+1) / len;
                int cnt = 2 + dp[l][l+len-1];  // () + 内容本身
                if (rep >= 100) cnt += 3;
                else if (rep >= 10) cnt += 2;
                else cnt += 1;
                dp[l][r] = min(dp[l][r], cnt);
            }
        }
    }
    cout << dp[0][n-1] << endl;
}

复杂度:\(O(n^{3})\)(检查循环节可用 KMP 优化到 \(O(n^{2})\))。

UVA1630 Folding

题目描述:与 P4302 类似,但需要输出折叠后的方案。紫题。

题目解析:在 dp 基础上增加 opt[l][r] 记录决策(>0为划分点,<0为折叠周期长度的负数)。用 KMP 的 prefix function 高效检测循环节。

string str;
while (cin >> str) {
    int n = str.size();
    const int inf = INT_MAX / 2;
    vector<vector<int>> dp(n, vector<int>(n, inf));
    vector<vector<int>> opt(n, vector<int>(n));
    for (int i = 0; i < n; i++) dp[i][i] = 1;
    for (int l = n-1; l >= 0; l--) {
        for (int r = l; r < n; r++) {
            int len = r - l + 1;
            // 划分
            for (int k = l; k < r; k++)
                if (dp[l][k] + dp[k+1][r] < dp[l][r])
                    dp[l][r] = dp[l][k] + dp[k+1][r], opt[l][r] = k;
            // KMP 找循环节
            vector<int> pi(len);
            for (int i = l+1, p = 0; i <= r; i++) {
                while (p && str[i] != str[l+p]) p = pi[l+p-1];
                if (str[i] == str[l+p]) pi[i] = ++p;
            }
            if (pi[r] != 0 && len % (len - pi[r]) == 0) {
                int subLen = len - pi[r];
                int rep = len / subLen;
                int cnt = 2 + dp[l][l+subLen-1];
                if (rep >= 100) cnt += 3;
                else if (rep >= 10) cnt += 2;
                else cnt += 1;
                if (cnt < dp[l][r]) dp[l][r] = cnt, opt[l][r] = -subLen;
            }
        }
    }
    auto output = [&](auto &self, int l, int r) {
        if (l == r) { cout << str[l]; return; }
        if (opt[l][r] < 0) {
            int cnt = (r-l+1) / -opt[l][r];
            cout << cnt << '(';
            self(self, l, l - opt[l][r] - 1);
            cout << ')';
            return;
        }
        self(self, l, opt[l][r]);
        self(self, opt[l][r]+1, r);
    };
    output(output, 0, n-1);
    cout << endl;
}

复杂度:\(O(n^{2})\)(KMP 优化循环节检测)。

洛谷 P4805 合并饭团

题目描述:CCC 2016。合并连续区间,两端的和需等于中间的和。问最大能合并出多大的值。

题目解析:dp[l][r] 表示 [l, r] 能否合并(1/0)。双指针收缩检查两端和是否相等且中间可合并。

int n; cin >> n;
vector<int> a(n+1);
for (int i = 1; i <= n; i++) cin >> a[i];
vector<int> sum(n+1);
partial_sum(a.begin(), a.end(), sum.begin());
vector<vector<int>> dp(n+1, vector<int>(n+1));
int ans = 0;
for (int l = n; l >= 1; l--) {
    dp[l][l] = 1; ans = max(ans, a[l]);
    for (int r = l+1; r <= n; r++) {
        int ll = l, rr = r;
        while (ll < rr) {
            int left_sum = sum[ll] - sum[l-1];
            int right_sum = sum[r] - sum[rr-1];
            if (left_sum == right_sum) {
                if (dp[l][ll] && dp[rr][r] && (dp[ll+1][rr-1] || ll+1 == rr)) {
                    dp[l][r] = 1; break;
                }
                ll++, rr--;
            } else if (left_sum < right_sum) ll++;
            else rr--;
        }
        if (dp[l][r]) ans = max(ans, sum[r] - sum[l-1]);
    }
}
cout << ans << endl;

复杂度:\(O(n^{3})\)

洛谷 P9746 合并序列

题目描述:KDOI-06-S。给定序列,执行操作:选择相邻三个元素 a,b,c,替换为 (a xor b xor c),代价为 max(a,b,c)。可以操作多次,问能否变为单个元素;若能,输出方案。紫题。

题目解析:dp[l][r] 记录 [l, r] 可合并为的异或值 k。用 sub[l][k](以 l 为左端点的最小右端点)、suf[r][k](以 r 为右端点的最大左端点)、comb[l][k](双区间合并的最远端点)来高效判定。本质是"三层嵌套"的区间DP。

const int limit = 512; int T; cin >> T;
while (T--) {
    int n; cin >> n;
    vector<int> arr(n+1), sum(n+1);
    for (int i = 1; i <= n; i++) cin >> arr[i];
    for (int i = 1; i <= n; i++) sum[i] = sum[i-1] ^ arr[i];
    const int invalid = INT_MAX;
    vector<vector<int>> sub(n+1, vector<int>(limit, invalid));
    vector<vector<int>> comb(n+1, vector<int>(limit, invalid));
    vector<vector<int>> suf(n+1, vector<int>(limit, INT_MIN));
    vector<vector<int>> dp(n+1, vector<int>(n+1, invalid));
    for (int l = n; l >= 1; l--) {
        dp[l][l] = -1;
        for (int r = l; r <= n; r++) {
            int xor_sum = sum[r] ^ sum[l-1];
            for (int k = 0; k < limit; k++)
                if (comb[l][k] < suf[r][k]) { dp[l][r] = k; break; }
            if (dp[l][r] != invalid) {
                suf[r][xor_sum] = max(suf[r][xor_sum], l);
                sub[l][xor_sum] = min(sub[l][xor_sum], r);
                if (r != n)
                    for (int k = 0; k < limit; k++)
                        comb[l][k] = min(comb[l][k], sub[r+1][k ^ xor_sum]);
            }
        }
        if (l != n)
            for (int k = 0; k < limit; k++)
                sub[l][k] = min(sub[l][k], sub[l+1][k]);
    }
    if (dp[1][n] == invalid) cout << "Shuiniao" << '\n';
    else {
        cout << "Huoyu" << '\n';
        // 递归输出方案(省略)
    }
}

复杂度:\(O(n^{2} \cdot limit)\)


3. 代码模版

3.1 基础区间DP框架 (\(O(n^{3})\))

// dp[l][r] = [l, r] 区间的最优解
for (int l = n; l >= 1; l--) {
    dp[l][l] = init_val;  // 初始化单元素区间
    for (int r = l + 1; r <= n; r++) {
        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] + cost(l, r, k));
        }
    }
}
answer = dp[1][n];

3.2 环形区间DP框架

// 破环成链
for (int i = 1; i <= n; i++) a[n + i] = a[i];
// 区间DP
for (int l = 2 * n; l >= 1; l--)
    for (int r = l + 1; r <= 2 * n; r++)
        for (int k = l; k < r; k++)
            dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r] + cost(l, r, k));
// 滑动窗口统计
for (int i = 1; i <= n; i++)
    ans = min(ans, dp[i][i + n - 1]);

3.3 非完全区间DP框架

// Step 1: 区间DP — 计算每个连续段内部的最优解
for (int l = n; l >= 1; l--)
    for (int r = l; r <= n; r++)
        for (int k = l; k < r; k++)
            dp[l][r] = merge(dp[l][k], dp[k+1][r]);

// Step 2: 线性DP — 从前缀中选择不重叠段
for (int i = 1; i <= n; i++) {
    f[i] = f[i-1] + single_cost(i);
    for (int j = 1; j <= i; j++)
        if (can_form(j, i))
            f[i] = min(f[i], f[j-1] + dp[j][i]);
}
answer = f[n];

4. 技巧总结

  1. 区间DP答案不一定在 dp[1][n]:环形DP的答案在滑动窗口中取最值,非完全DP的答案在 f[n] 中。
  2. 破环成链是处理环形问题的标准技巧,将长度翻倍后做区间DP,再取长度为 n 的窗口的最值。
  3. 四边形不等式优化:当 cost 函数满足四边形不等式时,划分点 k 有决策单调性(opt[l][r-1] ≤ opt[l][r] ≤ opt[l+1][r]),可将 \(O(n^{3})\) 优化到 \(O(n^{2})\)。典型题:UVA10304。
  4. 输出方案:用 opt[l][r] 记录 dp[l][r] 的决策(划分点或折叠方式),最后递归输出。
  5. 迭代 vs 记忆化:迭代(l 倒序、r 正序)常数更小;状态复杂的题目(如多维消除类)记忆化搜索代码更简洁。
  6. 祖玛类通用模式f[l][r][c] 记录前缀信息(与 a[l] 同色的元素个数),核心转移 f[l][r][c] ← f[l][k][c-1] + dp[k+1][r-1](需 a[l]a[k]a[r]),然后用 dp[l][r] = min(f[l][r][c] + cost(c)) 一起消除。
  7. 环形+祖玛:破环成链后做消除类区间DP,答案取滑动窗口最小值。典型题:AT_abc400_f。
posted @ 2026-05-26 12:13  Tangzy0121  阅读(16)  评论(1)    收藏  举报