题目描述:给定一个长为 \(n\) 的字符串 \(S\),求它所有前缀的循环移位最小表示法的开头位置,相同的输出靠前的一个。
数据范围:\(n\le 3\times 10^6\)
好像无论怎么想都跟朴素暴力一样是 \(O(n^2)\) 的...于是官方题解就开始分析性质...
我们考虑 \(k=1\rightarrow n\) 计算答案,并且只保留一些在将来有可能成为答案的点,其他的直接扔掉。我们称这些留下的点为候选点。
性质1:对于 \(i
这个很显然,就是 \(i,j\) 在 \(k\) 之前就已经比较出了大小,那么一定会淘汰一个。
性质2:对于 \(i
这个就需要考虑一下了,首先我们设 \(S=S_1S_1S_2\),其中 \(S_1,S_2\) 为 \(S\) 的两个子串。
则对于 \(S\) 的三个循环移位:\(S_1S_1S_2,S_1S_2S_1,S_2S_1S_1\),那么 \(S_1S_2S_1\) 一定比其他两个之一小,也就是说不可能为候选点。
那么如果有 \(k-j\ge j-i\),所以 \(S[1:k]=ABBC\),则设 \(S_1=B,S_2=CA\),也就是 \(BCAB\) 不可能成为答案,所以 \(j\) 不为候选点。
这样就可以得出任意两个候选点 \(i,j\),\(2(k-j)
然而为了方便计算还需要知道一点。
性质3:对于两个候选点 \(i
这个可以由性质1直接得出。
所以在比较候选点之间的大小的时候,不需要比较开始的一部分,剩下的可以转化为求两段后缀与整串的 lcp,这个就是 ExKMP 的 nxt 数组。
时间复杂度 \(O(n\log n)\),空间复杂度 \(O(n)\)。
#include
#define Rint register int
using namespace std;
const int N = 3000003;
int n, now, p0, nxt[N], ans;
char str[N];
vector cur, tmp;
inline int cmp(int p, int len){
return nxt[p] >= len ? 0 : (str[nxt[p]] < str[p + nxt[p]] ? 1 : -1);
}
inline int cmp(int x, int y, int len){
int res;
if(res = cmp(len - x + y, x - y)) return res > 0 ? x : y;
if(res = cmp(x - y, y)) return res > 0 ? y : x;
return y;
}
int main(){
scanf("%s", str); n = strlen(str);
nxt[0] = n; now = 0;
while(str[now] == str[now + 1] && now + 1 < n) ++ now;
nxt[1] = now; p0 = 1;
for(Rint i = 2;i < n;++ i){
if(i + nxt[i - p0] < p0 + nxt[p0]) nxt[i] = nxt[i - p0];
else {
now = max(0, p0 + nxt[p0] - i);
while(str[now] == str[now + i] && now + i < n) ++ now;
nxt[i] = now; p0 = i;
}
}
for(Rint k = 0;k < n;++ k){
tmp.resize(1); tmp[0] = k;
for(Rint i : cur){
while(!tmp.empty() && str[i + k - tmp.back()] < str[k]) tmp.pop_back();
if(tmp.empty() || str[i + k - tmp.back()] == str[k]){
while(!tmp.empty() && k - tmp.back() >= tmp.back() - i) tmp.pop_back();
tmp.push_back(i);
}
}
cur = tmp; ans = cur[0];
for(Rint i = 1;i < cur.size();++ i) ans = cmp(ans, cur[i], k + 1);
printf("%d%c", ans + 1, " \n"[k == n - 1]);
}
}