给定字符串 \(S\) ,对于 \(S\) 的每一个前缀 \(T\) 求 \(T\) 全部循环同构串的字典序最小的串,输出其起始下标。(若有多个输出最靠前的)html
\(|S| \le 3 \times 10^6\)c++
本文参考了官方题解。git
假设咱们如今考虑前缀 \(S[1 \dots k]\) ,咱们考虑哪些起始位置可能成为答案,咱们称做候选点。也就是对于这些候选点来讲,对于 \(i \ge k\) ,他们永远都会比非候选点更加优秀。数据结构
咱们首先能够经过不循环移位比出他们的字典序的话,确定能够直接看出哪一个必定不是候选点。学习
性质一:假设两个位置 \(i < j\) 。 若是 \(\mathrm{lcp}(S[i \dots n],S[j \dots n]) \le k − j\) , 那么 \(i,j\) 之间确定有一个不是候选点.优化
读者自证不难,利用上这个性质才是关键。spa
咱们假设获得 \(S[1 \dots k - 1]\) 的候选点集 \(P\) ,对于 \(i, j \in P, i < j\) 那么必定有 \(\mathrm{lcp}(S[i \dots n], S[j \dots n]) > (k - 1) - j\) ,咱们只须要找出是否存在一个 \(\mathrm{lcp}(S[i \dots n],S[j \dots n]) \le k − j\) 便可排除一个候选点。debug
咱们显然只须要比较 \(S[i + k - j]\) 与 \(S[k]\) 就能比出来了,可是枚举全部点对是十分浪费的一件事。咱们只考虑比较距离最远两个元素,留下较优的元素便可。code
而后这样看起来仍是 \(\mathcal O(n^2)\) 的,但彷佛能跑前 \(50pts\) 。(也许有更严谨的更优复杂度吧)htm
而后还须要利用一个神奇的性质优化候选点数。
性质二:对于两个点 \(i < j\) , 假设 \(\mathrm{lcp}(S[i \dots n],S[j \dots n]]) > k − j\) , 若是有 \(k − j \ge j − i\) , 那么 \(j\) 不是候选点。
这个性质看上去没有那么显然了。
证实:这个性质是有最小循环表示的某个性质得来的, 假设串 \(S = S_1 S_1 S_2\) ,其中 \(S_1, S_2\) 是任意两个子串。
- 要么有 \(S_1S_1S_2 \le S_1S_2 S_1 \le S_2S_1S_1\) 。
- 要么有 \(S_1S_1S_2 \ge S_1S_2 S_1 \ge S_2S_1S_1\) 。
这个讨论 \(S_1, S_2\) 字典序大小不难发现。
那么若是有 \(k - j \ge j - i\) 那么 \(S[1 \dots k]\) 形如 \(ABBC\) ,那么咱们把这两个后缀便可用 \(BBCA\) 和 \(BCAB\) 表示。把 \(S_1\) 设成 \(B\) ,\(S_2\) 设成 \(CA\) ,那么其实就是 \(S_1S_1S_2\) 与 \(S_1S_2S_1\) ,显而后者必定会被另外两个循环串包在中间,必定不如其余两个中的一个优。
利用上了这个性质,那么就有相邻两个候选点距离翻倍,那么只有 \(\mathcal O(\log n)\) 个候选点了。
这样的话,看似咱们能够利用各类后缀数据结构在 \(\mathcal O(n \log n)\) 内轻松愉悦的解决。
实则否则。。。除非你用 \(\text{SA-IS}\) ,那当我没说。
咱们预处理那里的复杂度要尽可能下降,咱们还须要知道一个性质。
性质三:对于任意两个候选点 \(i < j\) 那么 \(S[j \dots k]\) 是 \(S[i \dots k]\) 的一个前缀。
这个利用性质一不难发现。
那么咱们发现咱们每次其实只须要比较一个后缀和原串的字典序大小,这正好契合了 \(\mathrm{ExKmp}\) 的用途。
不会的话能够看我以前的学习笔记 qwq
而后预处理就变成 \(\mathcal O(n)\) ,总复杂度是 \(\mathcal O(n \log n)\) 。
求区间最小(循环)后缀,均可以考虑候选点只有 \(\mathcal O(\log n)\) 个的神奇性质。
#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; }