「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)\) 的算法。

小结:

  1. 处理字符串(序列)问题时,对于这种递推结构要敏感一点,并且应当相应地想到递推的算法
  2. \(P_k\) 的缩减其实也是来自于 Border 的循环结构,由同一循环生成的 Border 在很多情况下都是可以被直接略过的。当然这也需要对于 Border 的足够熟悉;
  3. 注意一下关于 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;
}

相关