区间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. 技巧总结
- 区间DP答案不一定在
dp[1][n]:环形DP的答案在滑动窗口中取最值,非完全DP的答案在f[n]中。 - 破环成链是处理环形问题的标准技巧,将长度翻倍后做区间DP,再取长度为 n 的窗口的最值。
- 四边形不等式优化:当 cost 函数满足四边形不等式时,划分点 k 有决策单调性(
opt[l][r-1] ≤ opt[l][r] ≤ opt[l+1][r]),可将 \(O(n^{3})\) 优化到 \(O(n^{2})\)。典型题:UVA10304。 - 输出方案:用
opt[l][r]记录dp[l][r]的决策(划分点或折叠方式),最后递归输出。 - 迭代 vs 记忆化:迭代(l 倒序、r 正序)常数更小;状态复杂的题目(如多维消除类)记忆化搜索代码更简洁。
- 祖玛类通用模式:
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))一起消除。 - 环形+祖玛:破环成链后做消除类区间DP,答案取滑动窗口最小值。典型题:AT_abc400_f。

浙公网安备 33010602011771号