最小表示法:双指针跳段比较 O(L) 求出循环串的字典序最小旋转,两个描述相等当且仅当最小表示相同。
OJ: roj
题目 ID: 3052
难度:普及+/提高-
标签:字符串最小表示法
创建: 2026-10-01 12:16
更新: 2026-10-07 13:50
形式化题目
给定两个长度相同(0–9 组成的字符串 0123 的四个旋转是 0123、1230、2301、3012。
判断 No;否则第一行输出 Yes,第二行输出
样例
输入:
2234342423
2423223434输出:
Yes
22343424232423223434 正是 2234342423 把末尾 4 位挪到最前得到的旋转,因此是同一条项链;它的最小表示就是输入中的第一个串。
正解
思路
朴素做法:枚举 Yes;最小表示则是
关键观察:要判断"同一条项链",其实不需要拿
最小表示法(双指针跳段):用两个起点指针
- 若
,说明两位打平, 加一继续比下一位; - 若
,则起点 落败。而且不止 :对任意 ,起点 与起点 的前 位完全相同,却在第 位更大,所以 整段都不可能成为最小表示的起点,直接令 ; 时对称地令 。
每次失配至少有一方前进
下表用样例中的串 22343424 模拟跳段过程,展示落败方如何整段或逐个被淘汰:
| 步骤 | 比较 |
结果 | 动作 | ||
|---|---|---|---|---|---|
| 1 | 0 | 1 | 平 | ||
| 2 | 0 | 1 | |||
| 3 | 0 | 3 | |||
| 4 | 0 | 4 → 5 → 6 | |||
| 5 | 0 | 6 |
第 2 步中起点 22343424 的最小表示。全程每个起点至多被淘汰一次,故为线性。
注意
代码
Python 版:
import sys
def min_rotation(s: str) -> str:
"""最小表示法(Lyndon / Booth 思想):返回循环串 s 的字典序最小旋转。
i, j 是两个候选起点,k 是当前匹配长度:
- s[i+k] == s[j+k]:k 继续延伸;
- s[i+k] < s[j+k]:j..j+k 都不可能成为更优起点,j 跳到 j+k+1;
- s[i+k] > s[j+k]:同理 i 跳到 i+k+1。
跳跃保证失败者整体后移,均摊 O(L)。
"""
s = list(map(ord, s))
n = len(s)
i, j, k = 0, 1, 0
while k < n and i < n and j < n:
d = s[(i + k) % n] - s[(j + k) % n]
if d == 0:
k += 1
else:
if d > 0:
i += k + 1 # i..i+k 全部落败,一起跳过
else:
j += k + 1
if i == j:
j += 1 # 两指针不能重合
k = 0
return ''.join(chr(c) for c in s[min(i, j):] + s[:min(i, j)])
def main() -> None:
data = iter(sys.stdin.buffer.read().split())
a, b = next(data).decode(), next(data).decode()
ma = min_rotation(a) # 两串相等当且仅当最小表示相等
if ma == min_rotation(b):
print('Yes')
print(ma)
else:
print('No')
main()C++ 版(同一算法):
/**
* 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-06 16:42
* update_at: 2026-10-06 16:42
*/
#include <cstdio>
#include <cstring>
const int MAXL = 1000005; // 项链长度上限 10^6,多留一点空间
char a[MAXL]; // 第一条项链的描述
char b[MAXL]; // 第二条项链的描述
// 求循环串 s(长度为 n)的字典序最小旋转的起点下标。
// i、j 是两个候选起点,k 是当前已连续匹配的长度;
// 失配时落败的一方连同它后面的 k 个候选一起被淘汰,跳到 k+1 步之后,
// 每个起点至多被淘汰一次,因此均摊 O(n)。
int min_rotation(char *s, int n) {
int i = 0;
int j = 1;
int k = 0;
while (k < n && i < n && j < n) {
char ci = s[(i + k) % n];
char cj = s[(j + k) % n];
if (ci == cj) {
k++;
} else {
if (ci > cj) {
i += k + 1; // 起点 i..i+k 全部落败,整体跳过
} else {
j += k + 1; // 起点 j..j+k 全部落败,整体跳过
}
if (i == j) {
j++; // 两指针重合时错开
}
k = 0;
}
}
return i < j ? i : j;
}
int main() {
scanf("%s %s", a, b);
int n = strlen(a);
int pa = min_rotation(a, n);
int pb = min_rotation(b, n);
// 两条项链相同当且仅当它们的最小表示相同。
int same = 1;
for (int t = 0; t < n; t++) {
if (a[(pa + t) % n] != b[(pb + t) % n]) {
same = 0;
break;
}
}
if (same) {
printf("Yes\n");
for (int t = 0; t < n; t++) {
putchar(a[(pa + t) % n]);
}
putchar('\n');
} else {
printf("No\n");
}
return 0;
}复杂度
- 时间:两个指针在环上至多各前进
步,失配时跳过的每一步都对应一个被永久淘汰的起点,总均摊 ;对 、 各做一次再加一次字符串比较,仍为 , 轻松通过。 - 空间:把字符串转成码点列表比较以便逐位取模访问,
。
总结
- 判断两个循环串描述是否同构,不必逐旋转比对:各自求最小表示(规范形)再比较即可,这一步把"与
匹配"降成"与规范形匹配"。 - 最小表示法用双指针 + 失配跳段:失配时落败方连同其后
个起点一起被证明不可能最优,一次跳 步,均摊线性,远优于 的朴素枚举。 - 实现要点:环上访问用取模;
时强制错开; 由循环条件自然覆盖。 - 同类模型:求循环串的最大表示(比较方向取反)、循环同构判断(
uvwu拼接后找单次出现)、以及 Lyndon 分解,都建立在同一套双指针跳段思想上。