字符串专题(一) 简单kmp 循环节 字典树 next数组应用

简单理解kmp

由于脑子不够用,所以只能粗略的说一下kmp

kmp是用于做字符串匹配的

应用: 1.匹配模式串在主串中第一个位置    2.模式串在主串中出现的次数    3.求循环节长度/ 最小覆盖子串长度    4.求串的最大前缀-后缀或次数或每个长度(即相同的前缀和后缀)    等等

首先我们先想一下用暴力进行匹配(tlen是模式串的长度,slen是主串的长度)

从左到右依次匹配,首先是从0到0+tlen,然后1到1+tlen......i到i+tlen 这样的话复杂度就很高为 O(slen*tlen)

运用kmp算法主要是解决对于指向模式串的下标的回溯问题.不理解看下面

要理解KMP的关键是要知道next数组的含义. next[i]是存放前 i个字符中前缀和后缀匹配的最大长度,如下

在模式串 abcab中 对应的next数组为

下标012345
字符abcab 
next-100012

一定要注意next[i]数组的含义

在主串 abcacbbabcabca中要匹配模式串

a b c a c b b a b c a b c a
a b c a b

从左向右依次匹配 匹配了前四个字符之后第五个字符不匹配,那么就需要进行下一步. .如果下一步从主串第二个元素b开始的话是很不合适的,因为很明显模式串的第一个字符是a.

那么从哪开始呢. 也很明显是从主串的第四个元素a开始更为合适..

我们之前在kmp算法之前处理了next数组 是的next[4]是1 ,这是什么意思. 在模式串中前4个字符长度为1的前缀和长度为1的后缀是匹配的.. next[4]是我们事先处理好的.  所以这里直接将指向模式串的下标指向next[4].  即如下图

a b c a c b b a b c a b c a
         a b c a b

此刻我们不需要再考虑红色之前的是否匹配.   因为这是上面可以保证匹配的.

(这里就是kmp算法的关键.在此不太懂一定要再去理解一下next数组的含义)

kmp算法模板的代码如下 HDU - 1711  HDU - 1686   HDU - 2087  这是一些模板题

void getNext(){//a数组是主串  b数组是模式串
	Next[0] = -1;
	int j = -1;
	int i = 0;
	while (i < tlen && j < tlen){
		if (j == -1 || b[i] == b[j]){
			Next[++i] = ++j;
		} else{
			j = Next[j];
		}
	}
}
int KMP(){
	int j = 0;
	int i = 0;
	getNext();
	while (i < slen && j < tlen){
		if (j == -1 || a[i] == b[j]){//如果a[i]和b[j]匹配了,则继续向后判断
			j++; i++;
		} else{//如果不匹配,更新指向模式串的下标
			j = Next[j];//将next[j]赋值给模式串的下标 以便匹配 除已经匹配的前缀外 的所有字符
		}
	}
	if (j == tlen){
		return i - j + 1;
	} else{
		return -1;
	}
}

模式串在主串中出现的次数

在匹配完成结束后不能立即退出循环.应该利用next数组 将下标改为下一个地方

 

int KMP(){
	int res = 0;
	int num = 0;
	int i = 0, j = 0;
	getNext();
	while (i < slen){
		if (j == tlen){
			num++;
			j = Next[j];
		}
		if (j == -1 || S[i] == P[j]){
			++i; ++j;
		} else{
			j = Next[j];
		}
	}
	if (j == tlen) num++;
	return num;
}

循环节  

HDU - 3746    51Nod - 1347 

含义:如果无限小数小数点后,从某一位起向右进行到某一位止的一节数字循环出现,首尾衔接,称这种小数为循环小数,这一节数字称为循环节

通俗的讲也就是一个周期

假设字符串S的长度为len,则S存在最小循环节, 循环节的长度L=len-next[len],

如果len可以被len-next[len]整除,则表明字符串S可以完全由循环节循环节组成

如果不能,说明还需要再添加几个字母才能补全. 需要补的个数是循环个数 L-len%L = L-(len-L)%L = L-next[len]%L.

参考博客 https://www.cnblogs.com/chenxiwenruo/p/3546457.html

 

next数组的应用

找出所有的前缀-后缀 POJ - 2752 

其实挺简单,主要是看你对next数组的理解程度如何

下标0123456789101112131415161718
字符ababcababababcabab 
next-1001201234343456789

仔细观察会发现next[18]=9 是指前缀ababcabab 和后缀匹配

next[9]=4是前缀abab和后缀匹配  这里的下标从0-8的字符串S1为ababcabab 正好就是上一个的后缀S2. 所以S1的前缀和后缀匹配就是S1的前缀和S2的后缀匹配.  也就是 整个串的前缀和后缀匹配.  发现了这个规律. 看是否能够实现呢?

#include<iostream>
#include<cstdio>
#include<string.h>
using namespace std;
typedef long long ll;
const int MAXN = 400000;
const int INF = 0x3f3f3f3f;
char str[MAXN + 100];
int Next[MAXN + 100];
int a[MAXN];
void getNext(int len){
	int i = 0;
	int j = -1;
	Next[0] = -1;
	while (i < len && j < len){
		if (j == -1 || str[i] == str[j]){
			i++; j++;
			Next[i] = j;
		} else{
			j = Next[j];
		}
	}
}
int main(){
	while (scanf("%s", str) != EOF){
		int len = strlen(str);
		getNext(len);
		int m = len;
		a[0] = len;
		int index = 1;
		while (Next[m]){
			a[index++] = Next[m];
			m = Next[m];
		}
		for (int i = index - 1; i >= 0; i--){
			if (i != index - 1) printf(" ");
			printf("%d", a[i]);
		}
		printf("\n");
	}
	return 0;
}

找出所有前缀在字符串中出现的次数51Nod - 1277 

下标01234567
字符abababa 
next-1001234

5

长度为7的出现一次  next[7]=5 长度为5的前缀在长度为7的字符串中出现一次 此时res[7]=1 res[5]=res[7]=1

长度为6的出现一次 next[6]=4 长度为4的前缀在长度为6的字符串中出现一次 此时res[6]=1 res[4]=res[6]=1

长度为5的出现一次 next[5]=3 长度为3的前缀在长度为5的字符串中出现一次 此时res[5]=2 res[3]=res[5]=2

长度为4的出现一次 next[4]=2 长度为2的前缀在长度为4的字符串中出现一次 此时res[4]=2 res[2]=res[4]=2

长度为3的出现一次 next[3]=1 长度为1的前缀在长度为3的字符串中出现一次 此时res[3]=3 res[1]=res[3]=3

长度为2的出现一次 next[2]=0 长度为0不算前缀 res[2]=3

长度为1的出现一次 同2  res[1]=4

综上所述 长度为1出现4次,   2出现3次   3出现3次   4出现2次   5出现2次   6出现1次   7出现1次

所以可以得到如下的代码

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<string.h>
using namespace std;
typedef long long ll;
const int MAXN = 100000;
const int INF = 0x3f3f3f3f;
char str[MAXN + 1000];
int Next[MAXN + 1000];
int len;
ll res[MAXN + 1000];
void getNext(){
	int i = 0; int j = -1;
	Next[0] = -1;
	while (i < len && j < len){
		if (j == -1 || str[i] == str[j]){
			++i; ++j;
			Next[i] = j;
		} else{
			j = Next[j];
		}
	}
}
int main(){
	while (scanf("%s", str) != EOF){
		len = strlen(str);
		getNext();
		for (int i = len; i >= 1; i--){
			res[i]++;
			res[Next[i]] += res[i];
		}
		ll maxx = -INF;
		for (ll i = 1; i <= len; i++){
			maxx = max(maxx, res[i] * i);
		}
		printf("%lld\n", maxx);
	}
	return 0;
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值