值域很小而位置很大, 于是把区间 DP 的维度反转: dp[i][v] 存从 i 合成 v 的最短结束位置, 答案取最大的可达 v.
OJ: luogu
题目 ID: P3147
难度:提高
标签:动态规划区间DP状态设计
创建: 2026-06-19 18:46
更新: 2026-10-26 13:12
形式化题目
给定长度为
一次操作可以选一对相邻且相等的数, 把它们替换成一个比原值大
求经过若干次操作后, 序列中能出现的最大数值.
暴力解法
思路
最直接的做法是按题意搜索: 每一步枚举所有"相邻且相等"的位置, 合并它俩, 然后继续递归, 在所有分支里取最大值.
每一步的分支数是当前可合并位置的数量, 最多
用全
| 10 | 12 | 13 | 14 | 15 | 16 | |
|---|---|---|---|---|---|---|
| 耗时 | 0.004s | 0.008s | 0.016s | 0.11s | 0.41s | 4.3s |
平均每多一个元素, 耗时约乘
暴力解的价值在于两个用途: 确认自己理解了操作规则, 以及在正解写完后作为小数据对拍的标准答案.
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-06-19 18:47
* update_at: 2026-10-26 12:48
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 直接搜索所有合法的合并顺序:每一步枚举"相邻且相等"的位置,把它俩合成 x+1,
// 然后递归继续,取所有分支里的最大值。
// 分支数随层数指数增长,所以只适合 n <= 20 量级的小数据。
#include <bits/stdc++.h>
using namespace std;
// 在当前序列 v 上枚举所有可合并的一对,递归取最优。
vector<int> cur;
int best;
void dfs(const vector<int> &v) {
for (int x : v) {
if (x > best) {
best = x;
}
}
for (int i = 0; i + 1 < (int)v.size(); i++) {
if (v[i] != v[i + 1]) {
continue;
}
// 这一层选择:合并位置 i 和 i+1。
vector<int> next = v;
next[i] = next[i] + 1;
next.erase(next.begin() + i + 1);
dfs(next);
}
}
int main() {
int n;
scanf("%d", &n);
cur.resize(n);
for (int i = 0; i < n; i++) {
scanf("%d", &cur[i]);
}
best = 0;
dfs(cur);
printf("%d\n", best);
return 0;
}复杂度与瓶颈
时间随合并顺序的数量指数增长, 空间
瓶颈是枚举了操作顺序. 而题目只问"最大能到多少", 不关心怎么合并, 所以应该去找"某个值能不能被做出来"的条件, 而不是模拟过程.
正解
关键观察
先把区间能不能合成一个数说清楚. 若区间
- 把每个
换成权重 , 合并 在权重上正好是 , 即相加. - 整段合并不改变权重总和, 所以总和恒为
. - 若整段合成值
, 则 ; 而 是定值, 满足它的 唯一.
这个性质很重要: 它说明"能合成的值"不需要再当状态维度, 它由区间唯一决定.
再看最后一步. 若
于是可以只问一个问题: 从左端
先说明为什么盯住"最短"不会丢解. 这里有个比"最短"更强的性质: 对固定的起点
- 反设存在两个不同的结束位置
, 即 与 都能合成 . - 对
用"最后一步是两个相邻 合并", 存在切分 使 与 都合成 . - 比较
与 的长度: 两者都从 开始, 所以只有包含关系. - 若
, 则 整段落在 里. 区间 的权重和是 , 它不超过 的权重和 , 矛盾(因为 ). - 若
, 左段这一侧确实不矛盾( ), 但改看右段: 的权重和是 , 而它包含 整段, 后者权重和是 , 已经超过整段的权重和, 矛盾. - 两支都矛盾, 所以
, 结束位置唯一.
既然唯一, "最短结束位置"就是那个唯一的位置, 不存在"取短会漏掉更长方案"的风险. 这使状态定义变得干净: 对每个
约定
- 先把
合成一个 , 其中 ; - 再从
开始合成第二个 , 即要求 . - 拼起来后,
就是合成 的区间, 故 .
也就是
注意下标那一项是 dp[mid + 1][v - 1] 的原因.
(结束位置唯一、切分点唯一这两条, 已对
思路
朴素区间 DP 设
但两个观察指向同一个方向: 值域很小, 位置很多.
- 答案上界很小: 要合成比初始最大值
大 的值, 至少需要 个元素参与( 要 个, 每升一级数量翻倍), 所以答案不超过 . 代入 、 得答案 . - 位置的取值范围很大: 从
到 .
所以把两者的角色对调: 用值当状态维度(只有约
定义
这样状态数从
下表是样例 1(
| 起点 |
|||
|---|---|---|---|
| 1 | 1 | 2 | 0 |
| 2 | 2 | 3 | 4 |
| 3 | 3 | 0 | 0 |
| 4 | 0 | 4 | 0 |
读表时注意初始化是
转移只有两步查表. 要合成
下图画出这个转移是怎么由两个相邻的
flowchart LR A["dp[2][2] = 3<br/>区间 [2,3] 合成 2"] --> B["从位置 4 再合出一个 2<br/>dp[4][2] = 4"] B --> C["两个相邻的 2 合并<br/>dp[2][3] = 4"] C --> D["区间 [2,4] 合成 3"]
第一段只用了位置
按
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-26 12:46
* update_at: 2026-10-26 12:46
*/
// main.cpp:反转状态维度的 DP。朴素区间 DP 的状态是 (左端点, 右端点),共 O(n^2);
// 本题的值域很小(答案不超过 58),于是把"值"当状态维度,用"位置"当存下来的结果:
// dp[i][v] = 从位置 i 开始能整段合成值 v 时最短的结束位置(0 表示合成不出来)。
// 转移:先把 [i, mid] 合成一个 v-1,再接着把 [mid+1, …] 合成一个 v-1,两个 v-1
// 相邻就能合成 v。按 v 从小到大递推即可。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 262144 + 5; // n <= 262144
// 答案上界:初始值 <= 40,每次合并只让值涨 1,而长度为 n 的区间最多合并 n-1 次。
// n <= 2^18,所以最大值为 40 + 18 = 58,取 60 留出余量。
const int MAXV = 62;
int n;
int a[MAXN]; // a[i]:序列第 i 个数(1-based)
// dp[i][v]:从位置 i 开始,最短到哪个位置结束可以把这段区间合成一个值 v。
// 用 1-based 的结束位置,0 表示合成不出来。
int dp[MAXN][MAXV];
int main() {
scanf("%d", &n);
int ans = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
dp[i][a[i]] = i; // 单个元素本身就是 a[i],结束位置就是它自己
if (a[i] > ans) {
ans = a[i]; // 一个数都不合并时,答案至少是初始最大值
}
}
// 按值从小到大递推:dp[i][v] 只依赖 v-1 那一层。
for (int v = 2; v < MAXV; v++) {
for (int i = 1; i <= n; i++) {
int mid = dp[i][v - 1];
if (mid == 0) {
continue; // 左边这一段都合不出 v-1
}
if (mid + 1 > n || dp[mid + 1][v - 1] == 0) {
continue; // 右边凑不出第二个相邻的 v-1
}
dp[i][v] = dp[mid + 1][v - 1];
if (v > ans) {
ans = v;
}
}
}
printf("%d\n", ans);
return 0;
}复杂度
时间 MAXV = 62, 覆盖到
空间 int, 约
实测
总结
这题的转折点不是优化转移, 而是换掉状态维度. 区间 DP 的
判断这类题时, 一个通用信号是: 答案的取值范围能估出一个小上界, 且它远小于下标的取值范围. 这时就该考虑"值当状态、下标当答案"的反转. 本题的上界
图示解析
下图把整条思路从暴力到正解压缩成一条主线, 重点是标注出"为什么必须换维度".
flowchart TB BR["暴力搜索合并顺序<br/>每步枚举相邻相等的位置<br/>分支指数增长, n<=20 可用"] BR -->|"瓶颈: 枚举的是操作顺序, 而题目只要最大可达值"| OB["关键观察<br/>1. 区间合成值唯一(权重 2^a 相加)<br/>2. 最后一步是两个相邻 v-1 合并<br/>3. 答案上界约 58, 值域很小"] OB -->|"值域小(约60) 但位置大(n=262144)"| RV["反转状态维度<br/>dp[i][v] = 从 i 合成 v 的最短结束位置<br/>状态数 O(60n) 而非 O(n^2)"] RV --> TR["转移两次查表<br/>mid = dp[i][v-1]<br/>dp[i][v] = dp[mid+1][v-1]"] TR --> ANS["答案 = 所有非零 dp 中最大的 v<br/>再与初始 max a[i] 取大"]
图中第一层是暴力, 第二层是三个关键观察, 第三层是维度反转, 后两层是转移与输出. 真正决定复杂度的是第三层: 前两层只说明"状态该怎么定义", 而