算法竞赛进阶指南-55.周期

题目链接

算法竞赛进阶指南-55.周期

一个字符串的前缀是从第一个字符开始的连续若干个字符,例如 abaab 共有 55 个前缀,分别是 aababaabaaabaab

我们希望知道一个 NN 位字符串 SS 的前缀是否具有循环节。

换言之,对于每一个从头开始的长度为 iii>1i>1)的前缀,是否由重复出现的子串 AA 组成,即 AAAAAAA…AAA 重复出现 KK 次,K>1K>1)。

如果存在,请找出最短的循环节对应的 KK 值(也就是这个前缀串的所有可能重复节中,最大的 KK 值)。

输入格式

输入包括多组测试数据,每组测试数据包括两行。

第一行输入字符串 SS 的长度 NN

第二行输入字符串 SS

输入数据以只包括一个 00 的行作为结尾。

输出格式

对于每组测试数据,第一行输出 Test case # 和测试数据的编号。

接下来的每一行,输出具有循环节的前缀的长度 ii 和其对应 KK,中间用一个空格隔开。

前缀长度需要升序排列。

在每组测试数据的最后输出一个空行。

数据范围

2N10000002 \le N \le 1000000

输入样例:

3
aaa
4
abcd
12
aabaabaabaab
0

输出样例:

Test case #1
2 2
3 3

Test case #2

Test case #3
2 2
6 2
9 3
12 4

Method : KMP

nxt[i]nxt[i]的含义:对于字符串的前缀子串S[0,i]S[0, i]nxt[i]nxt[i]是该前缀子串所有相等的前后缀里长度的最大值。

T=nnxt[n]T = n - nxt[n],则TT是最小循环节,并且如果TnT\nmid n,则题目无解。

证明1:TT是一个最小循环节。

反证法:假设T<T\exists T' < T
那么有nT>nT=nxt[n]n - T' > n - T = nxt[n],与nxt[n]nxt[n]是所有相等的前后缀里长度的最大值这一点矛盾。

证明2:如果字符串中存在更大的循环节TT',则TT'必然是TT的整数倍,即TTT \mid T'

反证法:假设T>T\exists T' > T,且TTT \nmid T',
那么设d=gcd(T,T)d = gcd(T, T'),易知dTd \mid T,因此d<Td < T
由裴蜀定理,可知d=xT+yTd = xT + yT'成立,不妨假设x>0,y<0x > 0, y < 0,那么对于原字符串SS
in,Si=Si+T=Si+2T=...Si+xT=Si+xTT=Si+xT2T=Si+xTyT=si+d\forall i\in n, S_i = S_{i + T} = S_{i + 2 T} = ... S_i+xT = S_{i + xT -T'} = S_{i + xT - 2T'} = S_{i + xT - yT'} = s_{i + d}
得到Si=Si+dS_i = S_{i + d},由定义可知dd是一个循环节,又因为d<Td < T,与TT是最小循环节矛盾。

#include <string>
#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e6 + 7;

int n;
string s;

vector<int> get_next(string& p) {
    int m = p.size();
    
    vector<int> nxt(m);
    nxt[0] = -1;
    for (int i = 1, j = -1; i < m; i ++) {
        while (~j && p[i] != p[j + 1]) j = nxt[j];
        if (p[i] == p[j + 1]) j ++;

        nxt[i] = j;
    }
    
    return nxt;
}

int main() {
    int T = 1;
    while (scanf("%d", &n), n) {
        cin >> s;
        
        vector<int> nxt = get_next(s);
        
        printf("Test case #%d\n", T ++);
        for (int i = 1; i < n; i ++) {
            int t = i - nxt[i];
            if (t != 0 && (i + 1) % t == 0 && (i + 1) / t > 1) {
                printf("%d %d\n", (i + 1) , (i + 1) / t);
            } 
        }
        puts("");
    }
    return 0;
}

复杂度分析

  • 时间复杂度:O(2n){O(2n)}, 其中2n2n为函数get_next()get\_next()的时间复杂度。

  • 空间复杂度:O(n){O(n)}nxtnxt 数组实际上只用了O(n)O(n)的空间。

证明参考:https://www.acwing.com/video/4635/