p_i 给出每个前缀最后一段 Lyndon 串的起点;据此切块、逐块模拟 Duval 定出相对大小,再自右向左贪心还原字典序最小的串。
OJ: luogu
题目 ID: P9719
难度:NOI/NOI+/CTS
标签:字符串Lyndon 分解Duval 算法贪心构造
创建: 2026-10-02 15:23
更新: 2026-10-04 22:05
形式化题目
给定长度为
现在只给出
暴力解法
思路
题目要求的答案是一个「字符」序列,而字符之间只有相对大小关系有意义,所以最优解一定可以只使用
于是最朴素的做法就是:按字典序从小到大枚举所有长度为
按定义求
代码
/**
* 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-02 15:23
* update_at: 2026-10-02 15:23
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 做法:答案只依赖字符之间的相对大小,所以最优解一定可以只使用 1..n 这些字符。
// 枚举所有 s[1..n](按字典序升序),第一个满足 p 的即为字典序最小的答案。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10;
int n;
int p[MAXN]; // 输入的 p_i
int cur[MAXN]; // 当前枚举的串
int ans[MAXN]; // 找到的答案
bool found;
// 计算当前串 cur 的 p 数组,与输入比较
bool check() {
for (int j = 1; j <= n; ++j) {
// 在 s[1..j] 的所有后缀中找字典序最小者(后缀长度互不相同,最小值唯一)
int best = 1;
for (int x = 2; x <= j; ++x) {
// 比较 s[x..j] 与 s[best..j]
int i = x, k = best;
while (i <= j && k <= j && cur[i] == cur[k]) {
++i;
++k;
}
if (k > j) {
// s[best..j] 是 s[x..j] 的前缀,更短的后缀字典序更小
continue;
}
if (i > j) {
// s[x..j] 是 s[best..j] 的前缀,更小
best = x;
continue;
}
if (cur[i] < cur[k]) best = x;
}
if (best != p[j]) return false;
}
return true;
}
// 按字典序枚举所有串
void dfs(int dep) {
if (found) return;
if (dep > n) {
if (check()) {
found = true;
for (int i = 1; i <= n; ++i) ans[i] = cur[i];
}
return;
}
for (int c = 1; c <= n && !found; ++c) {
cur[dep] = c;
dfs(dep + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> p[i];
found = false;
dfs(1);
if (!found) {
cout << -1 << "\n";
} else {
for (int i = 1; i <= n; ++i) cout << ans[i] << " \n"[i == n];
}
}
return 0;
}复杂度
枚举
瓶颈
暴力完全没有利用
正解
关键观察
先回忆两个定义:
- 若字符串
严格小于它的所有真后缀,则称 是 Lyndon 串(等价于 是其所有循环同构串中严格最小者)。 - 任意字符串都能唯一分解为若干 Lyndon 串
,且满足 ,称为 Lyndon 分解。
由此得到本题的突破口:
是 的最小后缀 是 的 Lyndon 分解中最后一段 的起点。
也就是说,
是整串最后一段 Lyndon 串的起点,记这段为 ,则 ; - 把它切掉,
又是一个前缀,它的最后一段起点是 ; - 反复执行
r = n; l = p[r]; r = l - 1,就能唯一地把整个串切成若干 Lyndon 串。
这一步是后面一切的基础:块与块之间的边界已经完全确定了,我们只需要在每块内部和块与块之间做决定。
思路
第一步:块内复用 Duval 算法。
Duval 算法在扫描一个 Lyndon 串时,只会遇到「
在单个块 len,以及 Duval 的 pre[i] = i - len,则:
| 条件 | 含义 | 记录 |
|---|---|---|
| 从 |
val[i] = 1,并令 len = i-l+1 |
|
| 最小后缀整体平移,即 |
val[i] = 0 |
|
| 其它 | 该前缀的 Lyndon 结构无法由单个块解释 | 无解 |
另外块内每个位置都要满足
为什么第二条等价于
第二步:块内最小赋值。
块首
即「相等就抄过来,严格小于就在抄来的基础上
第三步:块与块之间的约束与贪心。
Lyndon 分解要求 nb[1..m],让当前块在「
初始取块首 nb 的第
- 若
:当前块已经确定比右边块大,后面的位可以随意取最小值,停止调整(记 gt = true)。 - 若
:还需要补救,分两类—— val[i] = 1:这一位是自由的,直接把抬到 ,继续往后比较; val[i] = 0:这一位必须等于,不能单独改。只能往前找到最近的 val = 1位置(记作now),给加一,然后从 now+1重新按公式往后推一遍。由于now之前的位都和nb相等,加一后必然,于是同样停止调整。
- 若整块与
nb逐位相等但长度更短:说明当前块是右边块的前缀,字典序更小,仍需补救。此时把块内最后一个val = 1的位置加一,再重算它后面的位。
为什么贪心是对的? 固定右边块后,块首取值
代码
/**
* 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-02 15:23
* update_at: 2026-10-03 12:30
*/
// P9719 [EC Final 2022] Minimum Suffix
// 题意:给每个前缀的最小后缀起点 p_i,还原字典序最小的串 s(字符用正整数表示),无解输出 -1。
// 核心:p_i 就是前缀 i 的 Lyndon 分解中最后一个 Lyndon 串的起点。
// 于是从后往前把整个串切成 Lyndon 串,再在块内模拟 Duval,最后从右往左贪心。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 3000005;
int n;
int p[MAXN]; // 输入的 p_i
int pre[MAXN]; // Duval 中的 j 指针,s[i] 由 s[pre[i]] 推出
unsigned char val[MAXN]; // 1:s[i] = s[pre[i]] + 1(s_j < s_k);0:s[i] = s[pre[i]](s_j = s_k)
int s[MAXN]; // 答案串,字符从 0 开始编号
int nb[MAXN]; // 右边相邻 Lyndon 串(next block)的字符
// 手写快读,输入输出可达 3e6 规模
namespace fastio {
const int IBUF = 1 << 22;
char ibuf[IBUF];
int iidx, ilen;
inline int gc() {
if (iidx == ilen) {
ilen = (int)fread(ibuf, 1, IBUF, stdin);
iidx = 0;
if (ilen <= 0) return -1;
}
return (unsigned char)ibuf[iidx++];
}
inline int readInt() {
int c = gc();
while (c < '0' || c > '9') {
if (c == -1) return -1;
c = gc();
}
int x = 0;
while (c >= '0' && c <= '9') {
x = x * 10 + (c - '0');
c = gc();
}
return x;
}
const int OBUF = 1 << 22;
char obuf[OBUF];
int oidx;
inline void flushOut() {
if (oidx) {
fwrite(obuf, 1, oidx, stdout);
oidx = 0;
}
}
inline void putChar(char c) {
if (oidx == OBUF) flushOut();
obuf[oidx++] = c;
}
inline void writeInt(int x) {
if (x == 0) {
putChar('0');
return;
}
if (x < 0) {
putChar('-');
x = -x;
}
char tmp[16];
int t = 0;
while (x) {
tmp[t++] = char('0' + x % 10);
x /= 10;
}
while (t) putChar(tmp[--t]);
}
} // namespace fastio
// 在单个 Lyndon 串 [l, r] 内模拟 Duval 算法。
// 求出每个位置的 j 指针 pre[i] 以及 s_pre 与 s_i 的关系 val[i]。
// 返回 false 表示这个块无法由 p 解释,整组数据无解。
bool prepare_block(int l, int r) {
val[l] = 1; // 块首字符是自由变量,val 只作占位
// 块内每个位置的最小后缀都必须落在块内
for (int i = l; i <= r; ++i) {
if (p[i] < l) return false;
}
int len = 1; // 当前近似 Lyndon 串的周期长度
for (int i = l + 1; i <= r; ++i) {
pre[i] = i - len;
if (p[i] == l) {
// s_j < s_k,从 l 到 i 合并成一个新的 Lyndon 串
val[i] = 1;
len = i - l + 1;
} else if (i - p[i] == (i - len) - p[i - len]) {
// s_j == s_k,最小后缀起点整体平移 len
val[i] = 0;
} else {
return false; // 出现第三种情况,无解
}
}
return true;
}
// 构造块 [l, r] 的取值,要求它字典序 >= 右边相邻块 nb[1..m] 且尽量小。
void build_block(int l, int r, int m) {
int len = r - l + 1;
int gt = 0; // 当前块是否已经确定大于右边块
s[l] = (m >= 1) ? nb[1] : 0;
for (int i = l + 1; i <= r; ++i) {
s[i] = s[pre[i]] + val[i];
int t = i - l + 1;
int c = (t <= m) ? nb[t] : 0; // 右边块第 t 个字符,越界视为最小字符 0
if (s[i] > c) {
gt = 1;
}
if (!gt && s[i] < c) {
if (val[i]) {
// 这一位自由,直接抬到与右边块相等
s[i] = c;
} else {
// 这一位必须等于 s[pre[i]],只能把前面最近的自由位加一
int now = i;
while (val[now] != 1) --now;
i = now; // 从 now 之后重新推一遍
s[now]++;
gt = 1;
}
}
}
if (!gt && len < m) {
// 整块是右边块的前缀,字典序更小,抬高最后一个自由位
int now = r;
while (val[now] != 1) --now;
s[now]++;
for (int i = now + 1; i <= r; ++i) s[i] = s[pre[i]] + val[i];
}
}
// 处理一组数据,返回是否存在解
bool solve_case() {
n = fastio::readInt();
for (int i = 1; i <= n; ++i) p[i] = fastio::readInt();
// 从后往前切分 Lyndon 串:block = [l, r],其中 l = p[r]
int r = n;
int m = 0; // 右边相邻块的串长
while (r >= 1) {
int l = p[r];
if (!prepare_block(l, r)) return false;
build_block(l, r, m);
// 当前块成为下一轮的“右边块”
m = r - l + 1;
for (int i = l; i <= r; ++i) nb[i - l + 1] = s[i];
r = l - 1;
}
return true;
}
int main() {
int T = fastio::readInt();
while (T--) {
if (!solve_case()) {
fastio::putChar('-');
fastio::putChar('1');
fastio::putChar('\n');
} else {
for (int i = 1; i <= n; ++i) {
fastio::writeInt(s[i] + 1);
fastio::putChar(i == n ? '\n' : ' ');
}
}
}
fastio::flushOut();
return 0;
}复杂度
每个块内 Duval 模拟、构造、以及向左寻找 val = 1 的调整都是线性的(块内指针只单调左移,均摊
样例推演
以 1 2 2 为例(样例第 5 组)。先切块:
从右往左:右边块 val[3]=1(因为 0 1。
再处理左边块 gt = false,且块长 0 是 0 1 的前缀、偏小,于是把块内最后一个 val = 1 的位置(就是
最终 2 1 2,与样例一致。
总结
本题的关键是把「最小后缀起点」翻译成「Lyndon 分解末块起点」,从而把
- 用
r = n, l = p[r]迭代切出所有 Lyndon 块; - 块内用
与 判定 val并模拟 Duval,顺便判无解; - 自右向左贪心,块内按
取最小,遇到「小于右邻块」时把最近的 val = 1位置加一,即可在内得到字典序最小解。
难点不在复杂度,而在于能否看懂