AcWing 141 周期

题目描述:

一个字符串的前缀是从第一个字符开始的连续若干个字符,例如”abaab”共有5个前缀,分别是a, ab, aba, abaa, abaab。我们希望知道一个N位字符串S的前缀是否具有循环节。换言之,对于每一个从头开始的长度为 i (i>1)的前缀,是否由重复出现的子串A组成,即 AAA…A (A重复出现K次,K>1)。如果存在,请找出最短的循环节对应的K值(也就是这个前缀串的所有可能重复节中,最大的K值)。

输入格式

输入包括多组测试数据,每组测试数据包括两行。第一行输入字符串S的长度N。第二行输入字符串S。输入数据以只包括一个0的行作为结尾。

输出格式

对于每组测试数据,第一行输出 “Test case #” 和测试数据的编号。接下来的每一行,输出具有循环节的前缀的长度i和其对应K,中间用一个空格隔开。前缀长度需要升序排列。在每组测试数据的最后输出一个空行。

数据范围

2≤N≤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

分析:

本题考察KMP算法。最经典的一种字符串匹配问题就是比如有文本串T:abcabcabcd和模式串P:abcabcd,求模式串P是否是T的子串?

暴力的做法就是双指针法,实现i指向T的第一个元素,j指向P的第一个元素,i,j指向元素相等则i和j均加一,否则i++,j = 0,即从模式串的开头重新开始匹配,当j到达模式串末尾说明匹配成功,否则匹配失败。设T的长度为n,P的长度为m,该算法的时间复杂度为O(n * m)。

如何优化暴力的匹配过程?只需在i,j指向元素不等时不再从头比对就,而是利用已经匹配过元素的性质即可。还是以文本串T:abcabcabcd和模式串P:abcabcd为例,开始时,我们发现T和P的开头前六个元素abcabc均匹配,在第七个元素的位置a != d失配,于是按照暴力的匹配办法,从头开始匹配,模式串首元素a去与文本串第二个元素b比较,不等则i++,a与c比较,还不等i++,a = a,发现匹配,于是i++,j++,然后发现一直匹配,匹配结束。可以发现,在第一次匹配过程中,T的子串abcabc已经是和P的子串abcabc完全匹配了,所以第二次匹配过程的P与T的子串bcabc,cabc,abc匹配的比较都相当于是P本身与自己的子串在比较。在第一次匹配过程中,我们预知了第二次匹配会遇到的元素,而且知道,只有以i指向的元素加上后两个元素为abc时,才会和模式串前三个元素匹配上,或者说,第二次匹配的过程,从i指向b,c,a直至后面abc都匹配的过程都是没有必要的。对于P:abcabcd而言,在第七个字符上失配了,当T子串的前缀也为abc时才会再次匹配T的后缀abcd。

如上图所示,在d位置失配,d之前的子串abcabc的真前缀abc与真后缀abc相等,所以可以快速右移,i不再从T的第二个位置开始匹配,j也不再从头开始匹配,i只需停留在失配的位置,而j跳到P的第四个字符a的位置开始匹配。我们维护一个next数组,使得next[i] = j表示在模式串的第i个元素失配,下次匹配直接从第j个元素开始比较。

其实KMP算法利用的正是本题所求的循环节的性质,比如abcabcabc,循环节为abc,真前缀与真后缀相等的最长子串是abcabc,即:

最短循环节长度为3,重复3次,可以发现,对于字符串abcabcabc而言,在最后一个位置上失配j会立刻指向第6个字符,即前后缀相等的最所有前后缀中最长的长度为6,即next[9] = 6,表示在第9个字符上失配立刻跳到第6个字符上开始重新比对。发现字符串长度为9,next[9] = 6,9 - 6 = 3恰好是循环节的长度。这里有个规律:当字符串长度为len时,next[len] = j,当len - j能被len整除时,len - j就是最小循环节的长度。

此处的证明比较繁琐:为了方便,调整下变量的定义:设s为字符串的长度,next[s] = j,len = s - j。由于模式串的前j个元素和后j个元素(相当于上图的abcabc)相等,且len能被s整除,则由于len + j = s,len能被s整除,说明j是len的倍数,比如上例s = 9,len = 3,j = 6。而且用str[1 ~ s - len]表示真前缀,str[len ~ s]表示真后缀,可以发现真前缀的前len个字符与真后缀的前len个字符相等,并且由于真前缀的长度j是len的倍数,所以对真前缀和真后缀每隔len长度就切分一下,其对应部分的子串也都是相等的。

如上图所示真前缀和真后缀的第一个len长度的子串相等,第二个len长度的子串也相等,并且由于下面的字符串是上面的字符串右移得到的,第一部分和第三部分是相等的,所以可以得到一二三部分长度为len的子串是相同的,也就是循环节的长度。

所以本题只用遍历下字符串的每个位置,将该位置的下标减去next的值看能否被下标整除,即可判断是否存在循环节了。

#include <iostream>
using namespace std;
const int maxn = 1000005;
int n;
char str[maxn];
int nxt[maxn];
void get_next(){
    for(int i = 2,j = 0;i <= n;i++){
        while(j && str[i] != str[j + 1])    j = nxt[j];//失配时跳到next[j]
        if(str[i] == str[j + 1])    j++;
        nxt[i] = j;
    }
}
int main(){
    int T = 1;
    while(scanf("%d",&n),n){
        scanf("%s",str + 1);
        get_next();
        printf("Test case #%d\n",T++);
        for(int i = 2;i <= n;i++){
            int t = i - nxt[i];
            if(i > t && i % t == 0) printf("%d %d\n",i,i / t);
        }
        puts("");
    }
    return 0;
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值