kuangbin专题KMP & 扩展KMP & Manacher

本文介绍了KMP算法在多个HDU在线判题系统中的应用,包括求字符串序列、查找模式串出现次数、剪裁花布条、求循环字符串长度以及寻找公共连续子串等。通过KMP算法的模板实现,解决了一系列字符串处理问题。

HDU - 1711 Number Sequence

思路

KMP模板

代码

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 1000010;
int s[N], p[N];
int ne[N];

int main() {
    int n, m;
    int T;
    scanf("%d", &T);
    while(T --) {
        scanf("%d %d", &n, &m);

        for(int i = 1; i <= n; i ++) {
            scanf("%d", s + i);
        }

        for(int i = 1; i <= m; i ++) {
            scanf("%d", p + i);
        }

        int ans = -1;

        for (int i = 2, j = 0; i <= m; i ++) {
            while(j && p[j + 1] != p[i])
                j = ne[j];
            if(p[j + 1] == p[i])
                j++;
            ne[i] = j;
        }

        for(int i = 1, j = 0; i <= n; i ++) {
            while(j && p[j + 1] != s[i])
                j = ne[j];
            if(p[j + 1] == s[i])
                j++;
            if(j == m) {
                ans = i - m + 1;
                break;
            }
        }

        printf("%d\n", ans);
    }

    return 0;
}

HDU - 1686 Oulipo

思路

求模式串在匹配串出现的次数
还是KMP模板

代码

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 1000010;
char s[N], p[N];
int ne[N];

int main() {

    int T;
    scanf("%d", &T);
    while(T --) {
        scanf("%s", p + 1);
        scanf("%s", s + 1);

        int n = strlen(s + 1);
        int m = strlen(p + 1);

        int ans = 0;

        for (int i = 2, j = 0; i <= m; i ++) {
            while(j && p[j + 1] != p[i])
                j = ne[j];
            if(p[j + 1] == p[i])
                j++;
            ne[i] = j;
        }

        for(int i = 1, j = 0; i <= n; i ++) {
            while(j && p[j + 1] != s[i])
                j = ne[j];
            if(p[j + 1] == s[i])
                j++;
            if(j == m) {
                ans++;
                j = ne[j];
            }
        }

        printf("%d\n", ans);
    }

    return 0;
}

HDU - 2087剪花布条

题意

问能从匹配串裁剪多少个模式串

思路

还还还是模板题
只需要当找到一个匹配的,将j = 0而不是j=ne[i]

代码

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 1010;
char s[N], p[N];
int ne[N];

int main() {

    while(~scanf("%s", s + 1)) {
        if(s[1] == '#')
            break;
        scanf("%s", p + 1);

        int n = strlen(s + 1);
        int m = strlen(p + 1);

        int ans = 0;

        for (int i = 2, j = 0; i <= m; i ++) {
            while(j && p[j + 1] != p[i])
                j = ne[j];
            if(p[j + 1] == p[i])
                j++;
            ne[i] = j;
        }

        for(int i = 1, j = 0; i <= n; i ++) {
            while(j && p[j + 1] != s[i])
                j = ne[j];
            if(p[j + 1] == s[i])
                j++;
            if(j == m) {
                ans++;
                j = 0;
            }
        }

        printf("%d\n", ans);
    }

    return 0;
}

HDU - 3746 Cyclic Nacklace

题意

求加多少字符能将其字符串变为 至少循环两次 的字符串
例如:
1.abcab
需要加一个c,-> abcabc

2.abcd
需要加abcd,-> abcdabcd

思路

最小循环节问题,求ne数组即可
令字符串的长度为n
最小循环节len = n - ne[n]
则分情况判断即可
若要添加的情况,则add = len - (ne[n] % len)
若不用添加的情况, add = 0

代码

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 100010;

char p[N];
int ne[N];

int main() {
    int T;
    scanf("%d", &T);
    while(T --) {
        scanf("%s", p + 1);
        int n = strlen(p + 1);
        // 求ne数组
        for (int i = 2, j = 0; i <= n; i ++) {
            while(j && p[j + 1] != p[i])
                j = ne[j];
            if(p[j + 1] == p[i])
                j++;
            ne[i] = j;
        }

        // 最小循环节长度
        int len = n - ne[n];
        // 需要添加的字符个数
        int ans;
        if(len != n && n % len == 0) {
            ans = 0;
        }
        else {
            ans = len - (ne[n] % len);
        }

        printf("%d\n", ans);
    }

    return 0;
}

HDU - 1358 Period

思路

和上题类似
记住公式(模拟一下就知道)
最小前缀循环节: len = i - ne[i]
具有完整的循环节判断:i % (len) == 0
并注意求循环次数时必须满足:i / (i - ne[i]) > 1,即循环次数不能为1

代码

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
const int N = 1000010;

char p[N];
int ne[N];

int main() {
    int id = 0;
    int n;
    while(~scanf("%d", &n) && n){
        printf("Test case #%d\n", ++ id);

        scanf("%s", p + 1);
        for (int i = 2, j = 0; i <= n; i ++) {
            while(j && p[j + 1] != p[i])
                j = ne[j];
            if(p[j + 1] == p[i])
                j++;
            ne[i] = j;
        }

        for (int i = 2; i <= n; i ++) { 
            if(i % (i - ne[i]) == 0 && i / (i - ne[i]) > 1) {
                printf("%d %d\n", i, i / (i - ne[i]));
            }
        }

        puts("");
    }

    return 0;
}

POJ - 2406 Power Strings

思路

还是最小循环节问题

代码

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>

using namespace std;

const int N = 1000010;

char p[N];
int ne[N];

int main() {
    while(~scanf("%s", p + 1) && p[1] != '.') {
        int n = strlen(p + 1);
        for (int i = 2, j = 0; i <= n; i ++) {
            while(j && p[j + 1] != p[i])
                j = ne[j];
            if(p[j + 1] == p[i])
                j++;
            ne[i] = j;
        }

        int len = (n - ne[n]);
        if(n % len == 0)
            printf("%d\n", n / len);
        else
            printf("1\n");
    }

    return 0;
}

POJ - 2752 Seek the Name, Seek the Fame

思路

运用ne[i]数组的性质
它记录的就是当前1~i序列 的最大相同前后缀的长度

代码

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>

using namespace std;

const int N = 400010;

char p[N];
int ne[N];
int stk[N];
int top;

int main() {
    while(~scanf("%s", p + 1)) {
        top = -1;
        int n = strlen(p + 1);
        for (int i = 2, j = 0; i <= n; i ++) {
            while(j && p[j + 1] != p[i])
                j = ne[j];
            if(p[j + 1] == p[i])
                j++;
            ne[i] = j;
        }

        stk[++top] = n;
        for (int i = ne[n]; i; i = ne[i]) {
            stk[++top] = i;
        }

        while(top != -1) {
            printf("%d ", stk[top--]);
        }
        puts("");
    }

    return 0;
}

题意

找公共连续子串

思路

暴力即可,数据过小,不用kmp加速都行
暴力枚举第一个串的所有子串,check其他串是否能匹配成功,若可以,则更新答案

代码

#include <cstdio>
#include <cstring>
using namespace std;
const int N = 65;
char s[11][N], p[N], ans[N];
int n, m, mx, t, k, ne[N];
void getNext() {
	ne[1] = 0;
	for (int i = 2, j = 0; i <= n; i++) {
		while (j && p[i] != p[j + 1]) j = ne[j];
		if (p[i] == p[j + 1]) j++;
		ne[i] = j;
	}
}
bool kmp(int m) {
    // kmp匹配加速
    getNext();
    for (int i = 1, j = 0; i <= 60; i++) {
		while (j && s[m][i] != p[j + 1]) j = ne[j];
		if (s[m][i] == p[j + 1]) j++;
		if (j == n) return true;//匹配成功 
	}
	return false;
}

// 对所有字符串进行匹配, 是否都存在模式串
bool ok() {
	for (int i = 1; i <= m; i++) {
		if (!kmp(i)) return false;
	}
	return true;
} 
int main() {
	scanf("%d", &t);
	while (t--) {
		mx = 0;
		scanf("%d", &m);
		for (int i = 1; i <= m; i++) scanf("%s", s[i] + 1); 
		//枚举所有子串
		for (int i = 1; i <= 60; i++) {
			for (int j = i; j <= 60; j++) {
				for (int k = i; k <= j; k++) p[k - i + 1] = s[1][k]; 
				n = j - i + 1; 
                // 因为之前p数组是复用的, 这里加个'\0',代表字符串到这结束, 防止后面的字符串操作出错
                p[n + 1] = '\0';
				if (ok()) {
					if (n > mx) { mx = n; memcpy(ans + 1, p + 1, sizeof(p));}
                    // 保证字典序小
					else if(n == mx && strcmp(ans + 1, p + 1) > 0) memcpy(ans + 1, p + 1, sizeof(p));
				}  
			}
		} 
		if (mx < 3) printf("no significant commonalities\n");
		else printf("%s\n", ans + 1);
	}
	return 0;
}

POJ - 2185 Milking Grid

题意

求最小循环矩阵

思路

还是最小循环节问题。
只不过一维变为二维

  1. 求每行的最小循环节,搞个LCM得ans1
  2. 求每列的最小循环节,搞个LCM得ans2
  3. 答案 = ans1 * ans2
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>

using namespace std;

const int N = 10010;
int n, m;
int ne[N];
char str[N][N];

int gcd(int a, int b) {
    return b ? gcd(b, a % b) : a;
}

int lcm(int a, int b) {
    return a / gcd(a, b) * b;
}

int getr(int pos) {
    memset(ne, 0, sizeof ne);
    for (int i = 2, j = 0; i <= m; i ++) {
        while(j && str[pos][i] != str[pos][j + 1])
            j = ne[j];
        if(str[pos][i] == str[pos][j + 1])
            j++;
        ne[i] = j;
    }

    return m - ne[m];
}

int getl(int pos) {
    memset(ne, 0, sizeof ne);
    for (int i = 2, j = 0; i <= n; i ++) {
        while(j && str[i][pos] != str[j + 1][pos])
            j = ne[j];
        if(str[i][pos] == str[j + 1][pos])
            j++;
        ne[i] = j;
    }

    return n - ne[n];
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++)
        scanf("%s", str[i] + 1);

    int ans1 = 1;
    for (int i = 1; i <= n; i ++) {
        ans1 = lcm(ans1, getr(i));
    }

    if(ans1 > m)
        ans1 = m;

    int ans2 = 1;
    for (int i = 1; i <= m; i ++)
        ans2 = lcm(ans2, getl(i));
    
    if(ans2 > n)
        ans2 = n;
    
    printf("%d\n", ans1 * ans2);
    
    return 0;
}
在ACM算法竞赛中,字符串匹配和回文查找是常见的问题类型,其中KMP算法Manacher算法是解决这类问题的重要工具。KMP算法全称为Knuth-Morris-Pratt算法,它通过预处理模式串来避免不必要的比较,显著提高了在主串中的搜索效率。算法的核心在于构建一个部分匹配表(也称为&ldquo;失配函数&rdquo;或&ldquo;next数组&rdquo;),该表记录了模式串中每个位置之前的子串中有多少个字符与前缀重合。在实际搜索过程中,一旦发生不匹配,算法就会利用这个表跳过尽可能多的字符,从而减少比较次数。 参考资源链接:[2018年更新:kuangbin分享的ACM算法模板及核心数学技巧](https://wenku.youkuaiyun.com/doc/6412b67fbe7fbd1778d46efd) 具体来说,KMP算法包含两个主要步骤:模式串预处理和模式匹配。在预处理阶段,计算部分匹配表;在匹配阶段,根据部分匹配表指导搜索过程,当模式串与主串在某个位置失配时,根据部分匹配表找到模式串中下一个可能匹配的位置,从而避免从头开始比较。KMP算法的时间复杂度为O(m+n),其中m是模式串长度,n是主串长度,相比朴素的字符串匹配算法O(m*n),效率有显著提升。 Manacher算法则是用来查找字符串中最长回文子串的高效算法。与KMP算法类似,Manacher算法也是通过避免重复计算来提高效率的,但它使用了不同的策略。Manacher算法的核心思想是利用已知的回文信息来加速查找新的回文子串。算法将字符串中每个字符的左右两侧扩展,以跳过已经确定为回文的字符,并使用一个数组来记录已经找到的回文半径,避免了复杂的边界条件判断。 在实现Manacher算法时,通常引入一个虚拟字符,将原始字符串转化为所有字符都在回文中心的情况,这样每个字符都有一个对称的伴侣字符,算法通过维护一个数组来记录从每个位置出发能扩展的最大回文半径。Manacher算法的时间复杂度为O(n),其中n是字符串的长度,这是它相较于其他回文查找算法如中心扩展法的优势所在。 在ACM竞赛中,掌握KMPManacher算法对于处理字符串相关问题至关重要。通过《2018年更新:kuangbin分享的ACM算法模板及核心数学技巧》这一资料,你可以获得这些算法的具体实现和应用场景分析,加深对算法原理的理解,并在实际问题中灵活运用,以达到事半功倍的效果。 参考资源链接:[2018年更新:kuangbin分享的ACM算法模板及核心数学技巧](https://wenku.youkuaiyun.com/doc/6412b67fbe7fbd1778d46efd)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值