反转只影响子串内部,故新串与原串的第一处不同必在区间内:两端字符不同时答案是 [s[l]>s[r]],相同时递归到去掉两端的子区间,按长度递推加滚动数组得到 O(n^2) 时间、O(n) 空间。
OJ: luogu
题目 ID: P9232
难度:普及
标签:动态规划区间 DP字符串递推回文
创建: 2026-10-03 10:40
更新: 2026-10-07 12:15
形式化题目
给定一个长度为 0–9 的字符串
统计满足
例如
暴力解法
思路
最直接的做法就是照抄题面:枚举所有
写成 brute.cpp 里的三层循环:
- 外层
从 到 ,中层 从 到 ,枚举一个子串; - 把原串复制到
t,用双指针i、j从两端向中间交换,完成原地反转; smaller_than(t, s)逐位比较,遇到第一处不同就返回该位是否更小;两串完全相同则返回false(因为要求严格小于)。
这个做法没有用到任何规律,所以不可能漏解或算错,但它也把「中间段
代码
/**
* 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-03 10:40
* update_at: 2026-10-03 10:40
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 做法完全照抄题面:枚举所有子串 s[l..r](l < r),真的把这段反转出来,
// 再逐位和原串比较,统计新数字严格更小的方案数。
// 时间 O(n^3)、空间 O(n),只适合 n 在几百以内的小数据。
#include <iostream>
#include <cstring>
using namespace std;
const int MAXN = 1005;
int n;
char s[MAXN]; // 原数字串
char t[MAXN]; // 反转子串后得到的新数字串
// 逐位比较两个等长的数字串:a 是否严格小于 b(允许前导零,等长时字典序就是数值序)
bool smaller_than(const char *a, const char *b) {
for (int i = 0; i < n; i++) {
if (a[i] != b[i]) {
return a[i] < b[i];
}
}
return false; // 完全相同,不满足严格小于
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> s;
n = (int)strlen(s);
long long ans = 0;
for (int l = 0; l < n; l++) {
for (int r = l + 1; r < n; r++) {
// 复制原串,再把 s[l..r] 原地反转
for (int i = 0; i < n; i++) {
t[i] = s[i];
}
for (int i = l, j = r; i < j; i++, j--) {
char tmp = t[i];
t[i] = t[j];
t[j] = tmp;
}
if (smaller_than(t, s)) {
ans++;
}
}
}
cout << ans << "\n";
return 0;
}复杂度
- 时间:
个下标对,每个用 完成复制、反转和比较,总 。 - 空间:
,只需要两个长度 的字符数组。
瓶颈
瓶颈不在枚举下标对(那只有
正解
关键观察
反转是局部操作:它只改动
两串的第一处不同,必然出现在
内部。
因为对任意
于是可以只盯住区间内部,定义
答案就是
为什么可以递推? 位置
情形一:
情形二:
在这一段的值是 , 在这一段的值是 本身。
「
看一个具体的例子,10,102,两端 102 得 201,新串 210201 比原串 210102 大。
递推的边界。 当
递推的方向。
状态表
把样例 — 表示
| 0 | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| 0 | — | 1 | 1 | 1 | 1 | 1 |
| 1 | — | — | 1 | 0 | 1 | 0 |
| 2 | — | — | — | 0 | 0 | 0 |
| 3 | — | — | — | — | 1 | 0 |
| 4 | — | — | — | — | — | 0 |
| 5 | — | — | — | — | — | — |
所有
: ,直接给 。 : ,两端不同,给 。 :两端 与 相等,于是读内层 ,所以 。 :两端 ,给 。
第 3 条正是「两端相同 → 看中间」的现场演示。
空间压缩
最直接的想法是开一张 int 表,但那是
按右端点
- 用
dp_prev[l]保存上一轮(右端点为)的答案,于是 dp_prev[l + 1]就是; - 本轮每个
写进 cur[l],并累加到ans; - 一轮结束把
cur整体复制到dp_prev,进入下一轮。
这样内存降到三个 dp_prev 与 cur 必须分开:
如果原地只用一个数组,本轮刚写好的
代码
/**
* 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-03 10:40
* update_at: 2026-10-03 10:40
*/
// P9232 [蓝桥杯 2023 省 A] 更小的数
// 记 f(l, r) = 「把子串 s[l..r] 反转后,新数字是否严格小于原数字」。
// 反转只改动 [l, r] 这一段,所以新串与原串的比较结果由**第一处不同**决定。
// 按位置从外向内看,第一处不同只能出现在三段之一:
// 1. 位置 l:新串是 s[r],原串是 s[l];
// 2. 位置 r:新串是 s[l],原串是 s[r];
// 3. 严格夹在中间的 s[l+1..r-1]。
// 情形 1 和 3 是互斥的,于是得到转移:
// 若 s[l] != s[r]:第一处不同就在位置 l,f(l, r) = [s[l] > s[r]];
// 若 s[l] == s[r]:位置 l、r 都相同,只能看中间,f(l, r) = f(l+1, r-1)。
// 当 r - l + 1 = 2 或 3 时中间为空串或单字符,反转后与原串完全相同,
// 这两个区间的 f 恒为 0(正好是 s[l]==s[r] 分支的边界)。
//
// 实现:按右端点 r 从小到大推进。f(l, r) 在两端相等时需要 f(l+1, r-1),
// 那是「右端点为 r-1」的上一轮结果,因此只要用 prev[] 保存上一轮的 f,
// 本轮结果写进 cur[],每轮结束再把 cur 复制给 prev,空间就是 O(n)。
// 答案 = 所有 l < r 的 f(l, r) 之和,最多 n(n-1)/2 个方案。
#include <iostream>
#include <cstring>
using namespace std;
const int MAXN = 5005;
int n;
char s[MAXN]; // 输入的数字串,下标 0..n-1
int dp_prev[MAXN]; // dp_prev[l] 保存上一轮(右端点为 r-1)的 f(l, r-1)
int cur[MAXN]; // cur[l] 保存本轮(右端点为 r)的 f(l, r)
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> s;
n = (int)strlen(s);
long long ans = 0;
// 枚举右端点 r,左端点 l 从 r-1 递减到 0
for (int r = 1; r < n; r++) {
for (int l = r - 1; l >= 0; l--) {
int v; // v = f(l, r)
if (s[l] != s[r]) {
// 第一处不同就在最左边的 l:反转后该位变成原来的 s[r]
v = (s[l] > s[r]) ? 1 : 0;
} else {
// 两端字符相同,整体是否更小取决于中间子串 s[l+1..r-1]
if (r - l + 1 >= 4) {
v = dp_prev[l + 1]; // f(l+1, r-1),上一轮已算好
} else {
v = 0; // 长度 2、3:中间为空串或单字符,反转后不变
}
}
cur[l] = v;
ans += v;
}
// 本轮结果成为下一轮需要的「右端点为 r」的历史答案
for (int i = 0; i < n; i++) {
dp_prev[i] = cur[i];
}
}
cout << ans << "\n";
return 0;
}复杂度
- 时间:外层
共 轮,第 轮做 次 转移,合计 ;每轮复制 cur到dp_prev的也包含在内。 时约 次转移,实测在 s 内返回。 - 空间:
s、dp_prev、cur各,总 ,与暴力的空间同阶。
总结
- 核心观察:反转只改动
,所以两串的第一处不同必定落在区间内部。 位置 就是这个区间的第一位候选人。 - 递推:两端字符不同时,第一处不同就在
, ; 两端相同时, 和 都被“对齐”,问题等价于去掉两端后的子区间, 。 - 边界:长度为 2、3 的区间反转后与原串完全相同,
,作为递推的自然终止。 - 实现:按右端点滚动,
dp_prev存上一轮的、 cur存本轮, 时间和空间都是与 的组合;不要省掉 cur只用单数组原地更新。 - 易错点:严格小于而不是小于等于(相同时不能计数);
不是合法操作; 答案上限 要用 long long; 前导零合法,等长串按字典序比较即可。
