字符串——KMP模板及习题(持续更新)

kmp模板

//kmp算法的主要作用在于对next数组的运用
//性质1:对于每一个长度len的子串,该子串的最小循环节为tmp = len-next[len],如果 len%tmp == 0 且 len != tmp , 那么该串就是以最小循环节长度为tmp,循环的,如 tmp = 3, len = 12的串“abcabcabcabc”。
//性质2:kmp的next不断向前递归的过程可以保证对于每一个当前前缀,都有一段后缀与之对应

Next[]数组的 Next[i] 记录的是模式串的0到 i-1 子串的前缀和后缀的相同的最长长度,递推来求。当 i = 0时就为-1。
在KMP代码中,将模式串右移动几位就是相当于 j 改变一下就行。
如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:1
2
3

//kmp算法的主要作用在于对next数组的运用
//性质1:对于每一个长度len的子串,该子串的最小循环节为len-next[len]
//性质2:kmp的next不断向前递归的过程可以保证对于每一个当前前缀,都有一段后缀与之对应
const int maxn = 1e6+50;
int next[maxn];
char str[maxn], mo[maxn];//分别为文本串和模式串
int n1, n2;//两串的长度

void GetNext()
{
    memset(next, -1, sizeof(next));
    //cin >> mo;
    n2 = strlen(mo);
    int i = 0, j = -1;
    while(i < n2){
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; next[i] = j;
        }
        else{
            j = next[j];
        }
    }
    /*
    for(int i = 0; i < n2; i++){
        cout << next[i] << " ";
    }
    cout << endl;
    */
    return;
}
/*
int KMP()
{
    int i = 0, j = 0;
    n1 = strlen(str);
    while(i < n1){
        if(j == -1 || str[i] == mo[j]){
            i++; j++;
        }
        else{
            j = next[j];
        }
        if(j == n2){
            return i - n2 + 1;
        }
    }
    return -1;
}
*/
int main()
{
    cin >> str;
    cin >> mo;
    GetNext();
    //int cur = KMP();
    return 0;
}

kmp习题

1.hdu1711 Number Sequence

链接:hdu1711
题意:给你T组数据,每组包括一个文本串,一个模式串,让你找模式串第一次在文本串里面出现的位置,匹配失败就返回-1
题解:就是一个裸的kmp。
代码:

const int maxn = 1e6+50;
int T, n, m;
int Next[maxn];
int str[maxn], mo[maxn];

void GetNext(){
    for(int i = 0; i <= m; i++){
        Next[i] = -1;
    }
    int i = 0, j = -1;
    while(i < m)
    {
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
    return;
}

int KMP()
{
    int i = 0, j = 0;
    while(i < n){
        if(j == -1 || str[i] == mo[j]){
            i++; j++;
        }
        else{
            j = Next[j];
        }
        if(j == m){
            return i - m + 1;
        }
    }
    return -1;
}

int main()
{
    scanf("%d", &T);
    while(T--){
        scanf("%d%d", &n, &m);
        for(int i = 0; i < n; i++)
            scanf("%d", &str[i]);
        for(int i = 0; i < m; i++)
            scanf("%d", &mo[i]);
        GetNext();
        int ans = KMP();
        printf("%d\n", ans);
    }
    return 0;
}

2.hdu1680 Oilipo

链接:hdu1680
题意:和上一题差不多,改变的是让你求模式串在文本串中出现的次数,匹配成功的时候前面用过的可以重复使用。
题解:就是在j == m,的时候,让j = Next[j] 就行了, 相当于匹配失败的时候,按规则移动就行。
代码:

const int maxn = 1e6+50;
int T;
int n, m;
int Next[maxn];
char str[maxn], mo[maxn];

void GetNext()
{
    for(int i = 0; i <= m; i++){
        Next[i] = -1;
    }
    int i = 0, j = -1;
    while(i < m){
        if(j == -1 || mo[i] == mo[j]){
            j++; i++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
    return;
}

int KMP()
{
    int i = 0, j = 0;
    int ans = 0;
    while(i < n){
        if(j == -1 || str[i] == mo[j]){
            i++; j++;
        }
        else{
            j = Next[j];
        }
        if(j == m){
            ans++;
            j = Next[j];//按匹配失败时的规则就行
        }
    }
    return ans;
}

int main()
{
    scanf("%d", &T);
    while(T--){
        scanf("%s", mo);
        scanf("%s", str);
        n = strlen(str);
        m = strlen(mo);
        GetNext();
        int ans = KMP();
        printf("%d\n", ans);
    }
    return 0;
}

3.hdu2087 剪花布条

链接:hdu2087
题意:和上一题差不多,但是匹配成功时,该匹配过的不能再次使用。
题解:匹配成功时,奖 j = 0 就行,相当于从匹配成功的下一个字符串重新从头开始匹配。
代码:

#include <bits/stdc++.h>

using namespace std;

const int maxn = 1e4+50;
int n, m;
int Next[maxn];
char str[maxn], mo[maxn];

void GetNext(){
    Next[0] = -1;
    m = strlen(mo);
    int i = 0, j = -1;
    while(i < m){
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
    return;
}

int KMP(){
    int ans = 0, i = 0, j = 0;
    n = strlen(str);
    while(i < n){
        if(j == -1 || str[i] == mo[j]){
            i++; j++;
        }
        else{
            j = Next[j];
        }
        if(j == m){
            ans++;
            j = 0;
        }
    }
    return ans;
}

int main()
{
    while(true){
        scanf("%s", str);
        if(str[0] == '#')
            break;
        scanf("%s", mo);
        GetNext();
        int ans = KMP();
        printf("%d\n", ans);
    }
    return 0;
}

4.hdu3764 Cycle Nackdace

链接:hdu3764
题意:T组,每组给你一个字符串,问你加上最少多长的字符串,可以让这个字符串变成循环串。
题解:直接求一下Next数组,判断一下最小循环串就行
代码:

void GetNext()
{
    Next[0] = -1;
    m = strlen(mo);
    int i = 0, j = -1;
    while(i < m){
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
    return;
}

int main()
{
    int T;
    scanf("%d", &T);
    while(T--){
        scanf("%s", mo);
        GetNext();
        int tmp = m - Next[m];
        //cout << Next[m] << endl;
        if(m%tmp == 0 && m != tmp){
            printf("0\n");
        }
        else{
            printf("%d\n", tmp-m%tmp);
        }
    }
    return 0;
}

5.hdu1358 Period

链接:hdu1358
题意:多组,每组一个字符串,问字符串的所有子串(包括本身)是否为循环串,若是则输出该子串的右侧的下标(从1开始),以及该子串循环次数是多少。
题解:求Next数组,最小循环tmp = i - Next[i],判断一下 i % tmp == 0 && tmp != i 就行。
代码:

void GetNext()
{
    Next[0] = -1;
    int i = 0, j = -1;
    n = strlen(mo);
    while(i < n){
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
    return ;
}

int main()
{
    int cnt = 1;
    while(scanf("%d", &n)){
        if(n == 0)
            break;
        scanf("%s", mo);
        GetNext();
        printf("Test case #%d\n", cnt++);
        for(int i = 1; i <= n; i++){
            int tmp = i - Next[i];
            if(i % tmp == 0 && tmp != i){
                printf("%d %d\n", i, i / tmp);
            }
        }
        printf("\n");
    }
    return 0;
}

6.poj2406 Power Strings

链接:poj2406
题意:多组,每组一个字符串,若该字符串为循环串则输出的最大循环次数,否则输出1。
题解:求Next数组,求解最小循环节就行。
代码:

void GetNext()
{
    Next[0] = -1;
    n = strlen(mo);
    int i = 0, j = -1;
    while(i < n){
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
    return ;
}

int main()
{
    while(true){
        scanf("%s", mo);
        if(mo[0] == '.' && strlen(mo) == 1){
            break;
        }
        GetNext();
        int tmp = n - Next[n];
        if(n % tmp == 0 && n != tmp){
            printf("%d\n", n / tmp);
        }
        else{
            printf("1\n");
        }
    }
    return 0;
}

7. poj2752 Seek the Name, Seek the Fame

链接:poj2752
题意:给你一个字符串,求出该字符串所有相同的前缀和后缀的长度,按从小到大排序
题解:关键是j = Next[j],的 j 就表示比当前相同的前缀和后缀短一些的上一个相同的长度,是对Next数组的运用。
我的收获:强强强
代码:

const int maxn = 1e6+50;
int n, m;
int Next[maxn], ans[maxn];;
char str[maxn], mo[maxn];

void GetNext()
{
    Next[0] = -1;
    m = strlen(mo);
    int i = 0, j = -1;
    while(i < m){
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
    return ;
}

int main()
{
    while(scanf("%s", mo) != EOF){
        //scanf("%s", mo);
        GetNext();
        int cur = 0, j = m;
        while(j != 0){
            ans[cur++] = j;
            j = Next[j];//关键
        }
        for(int i = cur-1; i >= 0; i--){
            printf("%d%c", ans[i], i == 0 ? '\n' : ' ');
        }
    }
    return 0;
}

8.hdu2594 Simpsons’ Hidden Talents

链接:hdu2594
题意:多组,每组俩个字符串,让你求出最长前一个字符串的前缀和后一个字符串的后缀相同的字符串以及长度,并输出。
题解:先将两个字符串做连接并求出Next,求出min(len1, len2) = Min,若求出来Next[len1+len2] >Min 肯定长了, 就根据Next数组的性质,不断递归找短一点的相同前缀和后缀长度,直到ans <= Min就行。
找的方式是 while(ans > min) { ans = Next[ans]; } 和上一题一样用到了该性质。

代码:

void GetNext(){
    Next[0] = -1;
    int i = 0, j = -1;
    while(i < m){
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
}

int main()
{
    while(scanf("%s%s", a, b) != EOF){
        n1 = strlen(a);
        n2 = strlen(b);
        m = n1 + n2;
        for(int i = 0; i < n1; i++){
            mo[i] = a[i];
        }
        for(int i = n1; i < m; i++){
            mo[i] = b[i-n1];
        }
        mo[m] = '\0';
        GetNext();
        int Min = min(n1, n2);
        int ans = Next[m];
        //cout << endl;
        while(ans > Min){
            //cout << ans << " " << Next[ans] << endl;
            ans = Next[ans];
        }
        for(int i = 0; i < ans; i++){
            printf("%c", mo[i]);
        }
        if(ans == 0){
            printf("0\n");
        }
        else{
            printf(" %d\n", ans);
        }
    }
    return 0;
}

9.hdu3336 Count the string

链接:hdu3336
题意:T组,每组给你一个n表示字符串长度,一个字符串,让你求该字符串的每一个前缀在该字符串中出现的次数,包括串本身。要求答案对10007取模。
题解:就是对该串求一个Next数组,从n开始到1,每次求该长度的字符串的相同的前缀和后缀,ans++,用到第二条性质,while(tmp != 0){ ans++; tmp = Next[tmp];}
代码:

int main()
{
    scanf("%d", &T);
    while(T--){
        scanf("%d", &m);
        scanf("%s", mo);
        GetNext();
        int ans = 0;
        for(int i = m; i >= 1; i--){
            int cur = i;
            //cout << endl << cur << endl;
            while(cur != 0){
                cur = Next[cur];
                //cout << cur << endl;
                if(cur != 0) ans += 1;
                ans %= 10007;
            }
        }
        ans = (ans+m) % 10007;
        printf("%d\n", ans);
    }
    return 0;
}

10.hdu4300 Clairewd’s message

链接:hdu4300
题意:T组,每组俩个字符串,第一个字符串26个字符,每一个位置字符表示 i + ‘a’ 的明文, 转化为str[i] 的暗文。第二个字符串表示给你的是一个暗文+一个不全的明文(或者已经全了),暗纹和明文是按第一个字符串表示的转化规则一一对应的,就是明文是不是全的不知道。现在让你输出完整的暗纹+明文。
题解:既然不知到明文全不全,那么明文的长度一定是小于等于第二个字符串长度m的一半的。那么先将(m+1)/2长度的暗文转化为明文,已有的明文肯定是已经转化过的暗纹的前缀。此时用Next数组求出该转化过的字符串的最长前缀和后缀长度。用第二条性质,while(ans > min((m+1)/2, m - (m+1)/2)) ans = Next[ans]; 求出具体的已有的明文长度,然后输出原来的第二个串,再按照ans输出剩余待不全的明文就行。
代码:这个题一开始我还准备改改求Next[] 的规则,发现好像不太像,后来看了题解才想出来明文长度小于等于m / 2 。

const int maxn = 2e5+50;
int T, n, m;
int Next[maxn];
char str[maxn], mo[maxn];
map<char, char> mm, nn;

void GetNext()
{
    Next[0] = -1;
    int i = 0, j = -1;
    while(i < m){
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
    return ;
}

int main()
{
    scanf("%d", &T);
    while(T--){
        scanf("%s%s", str, mo);
        n = strlen(str);
        m = strlen(mo);
        for(int i = 0; i < n; i++){
            mm[i+'a'] = str[i];
            nn[str[i]] = char(i+'a');
        }
        for(int i = 0; i < m; i++)
            str[i] = mo[i];//存下来原来的mo串
        int mid = (m+1) / 2;
        for(int i = 0; i < mid; i++){
            mo[i] = nn[mo[i]];
        }
        //cout << mo << endl;
        GetNext();
        int ans = Next[m];
        while(ans > min(mid, m-mid)){
            ans = Next[ans];
        }//第二条性质求明文长度
        //cout << ans << endl;
        for(int i = 0; i < m-ans; i++)
            printf("%c", str[i]);
        for(int i = 0; i < m-ans; i++)
            printf("%c", nn[str[i]]);
        printf("\n");
    }
    return 0;
}

11.hdu3374 String Problem

链接:hdu3374
关于最大最小表示法请看博客:放过@字符串的最大最小表示法
题意:多组输入,每组一个字符串,要求输出最小表示法的第一个位置和最小表示法出现的次数 以及 最大表示法的第一个位置和出现次数。
题解:按照上述方法求出最大最小表示法的第一个位置,如何判断出现的次数就要用大KMP的Next数组了,考虑要的到出现的次数,只有当字符串是循环出现的,才有可能在变换后出现相同的字符串。用Next的第一个性质,求出最小循环节tmp = len-Next[len], 判断len%tmp 是否为0,若为0,则表示是循环字符串,最大最小出现次数就是len/tmp。否则出现次数为1。
代码:

const int maxn = 1e6+50;
int m, n;
int Next[maxn];
char str[maxn], mo[maxn];

void GetNext()
{
    m = strlen(mo);
    Next[0] = -1;
    int i = 0, j = -1;
    while(i < m){
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
    return ;
}

int GetMin(char *s)
{
    n = strlen(s);
    int i = 0, j = 1, k = 0, t;
    while(i < n && j < n && k < n){
        t = s[(i+k)%n] - s[(j+k)%n];
        if(!t) k++;
        else{
            if(t > 0) i += k+1;
            else j += k+1;
            if(i == j) j++;
            k = 0;
        }
    }
    return i < j ? i : j;
}

int GetMax(char *s)
{
    n = strlen(s);
    int i = 0, j = 1, k = 0, t;
    while(i < n && j < n && k < n){
        t = s[(i+k)%n] - s[(j+k)%n];
        if(!t) k++;
        else{
            if(t > 0) j += k+1;
            else i += k+1;
            if(i == j) j++;
            k = 0;
        }
    }
    return i < j ? i : j;
}

int main()
{
    while(scanf("%s", mo) != EOF){
        GetNext();
        int Min = GetMin(mo) + 1;
        int Max = GetMax(mo) + 1;
        int tmp = m - Next[m];
        if(m % tmp == 0 && m != tmp){
            printf("%d %d %d %d\n", Min, m/tmp, Max, m/tmp);
        }
        else{
            printf("%d 1 %d 1\n", Min, Max);
        }
    }
    return 0;
}

12.fzu1901 Period II

链接:fzu1901
题意:n组,每组一个字符串,现在让你求所有长度p使得S[i]=S[i+P] for i in [0…SIZE(S)-p-1]。
题解:用kmp的Next数组第二个性质,一直递归求小一号长度相同的前缀和后缀的长度,答案就是用字符串长度m减去每一次递归的到的长度tmp,即m-tmp。注意的就是m也是答案之一,因为空对空也算。
代码:

const int maxn = 1e6+50;
int n, m;
int Next[maxn];
char mo[maxn];
int ans[maxn];

void GetNext()
{
    Next[0] = -1;
    m = strlen(mo);
    int i = 0, j = -1;
    while(i < m){
        if(j == -1 || mo[i] == mo[j]){
            i++; j++; Next[i] = j;
        }
        else{
            j = Next[j];
        }
    }
    return;
}

int main()
{
    scanf("%d", &n);
    int cur = 1;
    while(n--){
        scanf("%s", mo);
        GetNext();
        int tmp = Next[m];
        int sum = 0;
        while(tmp != 0){
            ans[sum++] = m-tmp;
            tmp = Next[tmp];
        }
        //ans[sum++] = m;
        printf("Case #%d: %d\n", cur++, sum+1);
        for(int i = 0; i < sum; i++){
            printf("%d ", ans[i]);
        }
        printf("%d\n", m);
    }
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章