题意
给定字符串 S ,对于 S 的每个前缀 T 求 T 所有循环同构串的字典序最小的串,输出其起始下标。(如有多个输出最靠前的)
|S|≤3×106
题解
本文参考了官方题解。
假设我们现在考虑前缀 S[1…k] ,我们考虑哪些起始位置可能成为答案,我们称作候选点。也就是对于这些候选点来说,对于 i≥k ,他们永远都会比非候选点更加优秀。
我们首先可以通过不循环移位比出他们的字典序的话,肯定可以直接看出哪个一定不是候选点。
性质一:假设两个位置 i<j 。 如果 lcp(S[i…n],S[j…n])≤k−j , 那么 i,j 之间肯定有一个不是候选点.
读者自证不难,利用上这个性质才是关键。
我们假设得到 S[1…k−1] 的候选点集 P ,对于 i,j∈P,i<j 那么一定有 lcp(S[i…n],S[j…n])>(k−1)−j ,我们只需要找出是否存在一个 lcp(S[i…n],S[j…n])≤k−j 即可排除一个候选点。
我们显然只需要比较 S[i+k−j] 与 S[k] 就能比出来了,但是枚举所有点对是十分浪费的一件事。我们只考虑比较距离最远两个元素,留下较优的元素即可。
然后这样看起来还是 O(n2) 的,但似乎能跑前 50pts 。(也许有更严谨的更优复杂度吧)
然后还需要利用一个神奇的性质优化候选点数。
性质二:对于两个点 i<j , 假设 lcp(S[i…n],S[j…n]])>k−j , 如果有 k−j≥j−i , 那么 j 不是候选点。
这个性质看上去没有那么显然了。
证明:这个性质是有最小循环表示的某个性质得来的, 假设串 S=S1S1S2 ,其中 S1,S2 是任意两个子串。
- 要么有 S1S1S2≤S1S2S1≤S2S1S1 。
- 要么有 S1S1S2≥S1S2S1≥S2S1S1 。
这个讨论 S1,S2 字典序大小不难发现。
那么如果有 k−j≥j−i 那么 S[1…k] 形如 ABBC ,那么我们把这两个后缀即可用 BBCA 和 BCAB 表示。把 S1 设成 B ,S2 设成 CA ,那么其实就是 S1S1S2 与 S1S2S1 ,显然后者一定会被另外两个循环串包在中间,一定不如其他两个中的一个优。
利用上了这个性质,那么就有相邻两个候选点距离翻倍,那么只有 O(logn) 个候选点了。
这样的话,看似我们可以利用各种后缀数据结构在 O(nlogn) 内轻松愉悦的解决。
实则不然。。。除非你用 SA-IS ,那当我没说。
我们预处理那里的复杂度要尽量降低,我们还需要知道一个性质。
性质三:对于任意两个候选点 i<j 那么 S[j…k] 是 S[i…k] 的一个前缀。
这个利用性质一不难发现。
那么我们发现我们每次其实只需要比较一个后缀和原串的字典序大小,这正好契合了 ExKmp 的用途。
不会的话可以看我之前的学习笔记 qwq
然后预处理就变成 O(n) ,总复杂度是 O(nlogn) 。
总结
求区间最小(循环)后缀,都可以考虑候选点只有 O(logn) 个的神奇性质。
代码
#include <bits/stdc++.h>
#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
using namespace std;
template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }
inline int read() {
int x(0), sgn(1); char ch(getchar());
for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
return x * sgn;
}
void File() {
#ifdef zjp_shadow
freopen ("3103.in", "r", stdin);
freopen ("3103.out", "w", stdout);
#endif
}
const int N = 3e6 + 1e3;
void Get_Next(char *S, int *next) {
int lenS = strlen(S + 1), p = 1, pos;
next[1] = lenS;
while (p + 1 <= lenS && S[p] == S[p + 1]) ++ p;
next[pos = 2] = p - 1;
For (i, 3, lenS) {
int len = next[i - pos + 1];
if (len + i < p + 1) next[i] = len;
else {
int j = max(p - i + 1, 0);
while (i + j <= lenS && S[j + 1] == S[i + j]) ++ j;
p = i + (next[pos = i] = j) - 1;
}
}
}
char S[N]; int lcp[N], n; vector<int> cur;
inline int cmp(int p, int len) {
return lcp[p] >= len ? 0 : (S[lcp[p] + 1] < S[p + lcp[p]] ? 1 : -1);
}
inline int cmp(int x, int y, int len) {
static int res; assert(x > y);
if ((res = cmp(y + (len - x + 1), x - y))) return res > 0 ? x : y;
if ((res = cmp(x - y + 1, y - 1))) return res > 0 ? y : x;
return y;
}
int main () {
File();
scanf ("%s", S + 1); n = strlen(S + 1); Get_Next(S, lcp);
For (k, 1, n) {
vector<int> tmp(1, k);
for (int i : cur) {
while (!tmp.empty() && S[i + k - tmp.back()] < S[k]) tmp.pop_back();
if (tmp.empty() || S[i + k - tmp.back()] == S[k]) {
while (!tmp.empty() &&
k - tmp.back() >= tmp.back() - i) tmp.pop_back();
tmp.push_back(i);
}
}
cur = tmp; int ans = cur[0];
For (i, 1, cur.size() - 1) ans = cmp(ans, cur[i], k);
printf ("%d%c", ans, k == kend ? '\n' : ' ');
}
return 0;
}
__EOF__
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】