「JSOI 2019」节日庆典
题目
点这里看题目。简要题意如下:
给定一个字符串 \(S\),对它的每一个前缀,求其最小表示对应的后缀的下标。
对于 \(100\%\) 的数据,满足 \(1\le |S|\le 3\times 10^6\)。
分析
显然,一个循环表示就是一个后缀拼上一个前缀。
容易想到这样一个算法:基于询问的递推关系,我们可以先通过后缀来筛选出备选答案。具体来说,对于前缀 \(S[1:k]\),维护 \(P_k\) 为所有极小后缀的集合。如果一个后缀 \(u\) 在 \(S[1:k]\) 中不是极小的,就说明存在另一个后缀 \(v\),使得 \(u>v\) 且 \(v\) 不是 \(u\) 的前缀。换言之,\(u\) 已经被保证不可能成为最优解了。
考虑从 \(P_k\) 递推到 \(P_{k+1}\),我们只需要考虑 \(P_k\) 中的元素在末尾添上 \(S[k+1]\) 后的大小关系,以及 \(S[k+1]\) 这个后缀的影响。根据 \(P_k\) 的构成,我们只需要考虑较短的后缀对于较长的后缀影响即可。
在需要回答问题的时候,我们枚举 \(P_k\) 中的后缀,并取对应循环表示的最小值。此时我们需要做的比较都是某个后缀和整个串比较,用扩展 KMP 预处理 LCP 之后即可快速完成。
现在这个算法仍然是 \(O(n^2)\) 的,用一个 aaa...aaa
就可以导致 \(|P_k|=k\),复杂度爆炸。不过,在 aaa...aaa
这种情况下,\(P_k\) 其实包含了一大堆无效的后缀,我们得想办法把它们丢掉。
再回顾一下 \(P_k\) 的构成:原始定义中,如果 \(u,v\) 都包含在了 \(P_k\) 里面,且 \(|u|>|v|\),那么一定有 \(v\) 是 \(u\) 的 Border。而如果熟悉 Border 的构成,我们就不难想到透过 Border 的等差结构来得到下面这个结论:
\(P_k\) 中只需要包含 \(O(\log k)\) 个后缀。
考虑其中的任意两个后缀 \(u,v\),我们可以说明当 \(2|v|>|u|>|v|\) 时 \(v\) 这个后缀是无效的。
道理很简单,由于 \(v\) 是 \(u\) 的 Border,所以应该有 \(u=AAB,v=AB\)。考虑 \(w=B\),我们可以发现 \(v\) 被夹在 \(u,w\) 之间,并且怎么也不可能被取到:设 \(c=S[1]\),如果有 \(uc>vc\) ,那么也就有 \(vc>wc\);反过来如果有 \(uc
,那么也就有 \(vc 。 所以我们完全可以筛去 \(v\)。最终不会存在满足 \(2|v|>|u|>|v|\) 的一对后缀,因此 \(P_k\) 中只会包含 \(O(\log k)\) 个元素。
这个结论的分析过程其实也告诉了我们该怎么去维护 \(P_k\),按照这个做法即可得到 \(O(n\log n)\) 的算法。
小结:
- 处理字符串(序列)问题时,对于这种递推结构要敏感一点,并且应当相应地想到递推的算法;
- \(P_k\) 的缩减其实也是来自于 Border 的循环结构,由同一循环生成的 Border 在很多情况下都是可以被直接略过的。当然这也需要对于 Border 的足够熟悉;
- 注意一下关于 LCP 的算法在字符串比较中的应用。
代码
#include
#include
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )
const int MAXN = 3e6 + 5;
template
void read( _T &x ) {
x = 0; char s = getchar(); bool f = false;
while( s < '0' || '9' < s ) { f = s == '-', s = getchar(); }
while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
if( f ) x = -x;
}
template
void write( _T x ) {
if( x < 0 ) putchar( '-' ), x = -x;
if( 9 < x ) write( x / 10 );
putchar( x % 10 + '0' );
}
template
_T MIN( const _T a, const _T b ) {
return a < b ? a : b;
}
int tmp[MAXN], tot;
int stk[MAXN], top;
int z[MAXN];
char str[MAXN];
int N;
int main() {
scanf( "%s", str + 1 );
N = strlen( str + 1 ), z[1] = N;
for( int i = 2, l = 1, r = 1 ; i <= N ; i ++ ) {
if( i < r ) z[i] = MIN( r - i, z[i - l + 1] );
while( i + z[i] <= N && str[1 + z[i]] == str[i + z[i]] ) z[i] ++;
if( i + z[i] > r ) l = i, r = i + z[i];
}
rep( i, 1, N ) {
tmp[tot = 1] = i;
per( j, top, 1 ) {
int u = stk[j], v = tmp[tot];
if( str[u + i - v] > str[i] ) continue;
if( str[u + i - v] < str[i] ) tot --;
tmp[++ tot] = u;
}
stk[top = 1] = tmp[tot];
per( j, tot - 1, 1 ) {
int lu = i - stk[top] + 1,
lv = i - tmp[j] + 1;
if( ( lv << 1 ) <= lu )
stk[++ top] = tmp[j];
}
int ans = stk[1];
rep( j, 2, top ) {
int u = ans, v = stk[j];
int p = u + i - v + 1;
if( z[p] < v - u )
ans = str[p + z[p]] <= str[1 + z[p]] ? u : v;
else {
p = 1 + v - u;
if( z[p] < u - 1 )
ans = str[1 + z[p]] <= str[p + z[p]] ? u : v;
else ans = u;
}
}
write( ans ), putchar( i == N ? '\n' : ' ' );
}
return 0;
}