扩展KMP的应用
扩展kmp在字符串匹配专题的应用很广,但很多时候也还是能用kmp、马拉车等字符串匹配算法代替,这篇文章是笔者为18年湖南省赛准备的复习文,文章整理了笔者遇到过的扩展kmp在一些方面的应用
- 题目中出现的字符串名称及其定义:
- 循环同构串: abcd->bcda->cdab->dabc ,将首字符逐一左移至字符串尾部,得到
的所有字符串。 - 后缀子串:Si表示以第i个字符为首的S的后缀字符串。
- 重复不重叠字符串:abcabc,abc重复了两次。书面表示为AAA (A表示字符串)
- 循环同构串: abcd->bcda->cdab->dabc ,将首字符逐一左移至字符串尾部,得到
- 应用分类:
- 定义题:裸题(求T与S的所有后缀子串的最长前缀公共子串长度)
FZU-1901
ZOJ 3587 - 回文前缀子串和回文后缀子串上的应用
HDU 3613 - 分治 + ex_kmp
HUNNU OJ 11565 可惜标程错了
- 定义题:裸题(求T与S的所有后缀子串的最长前缀公共子串长度)
一句总结: 大部分应用都是将自己与反串匹配,就能处理题目要求的字符串性质
一、定义题
FZU-1901
题意:字符串S,问满足S[i] = S[i+P],(i ∈[0,Size -P-1]),升序顺序输出P的取值。
很裸的ex_kmp的定义题!判断Next[j] == Size - j +1 ,以S[j]开头后缀子串是S的一个前缀子串。
/*********************************
*** FZU 1901 ex_kmp 的 Next数组
*********************************/
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <algorithm>
#include <math.h>
#include <map>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#define llt long long
using namespace std;
const int N = 1e6+7777;
int Next[N];
char s[N];
void get_Next(int n){
Next[0] = n;//以s[0]开头后缀子串显然是本串
int i=0;
while(i+1<n&&s[i]==s[i+1])++i;//前面有i+1个数相同
Next[1] = i;
int po = 1;// 到达最远比较位置
for(int i=2;i<n;++i){
if(i+Next[i-po]<Next[po]+po)//不能等于
Next[i] = Next[i-po];
else{
//if(i==9) cout<<i<<endl;
int j = max(0,Next[po]+po-i);
while(i+j<n&&s[j]==s[i+j])++j;
Next[i] = j;
po = i;
}
}
Next[n] = 0;
}
int ans[N];
int main(){
int T;
scanf("%d",&T);
int cas = 0;
while(T--){
scanf("%s",s);
int n = strlen(s);
get_Next(n);
//cout<<Next[9]<<endl;
int cnt = 0;
for(int i=1;i<=n;++i)
if(n-i==Next[i]) ans[++cnt] = i;
printf("Case #%d: %d\n",++cas,cnt);
for(int i=1;i<=cnt;++i)
printf("%d%c",ans[i]," \n"[i==cnt]);
}
return 0;
}
zoj 3587
题意:求S的两个子串拼接成T的方案数。
ex_kmp 统计S的子串与T的前后缀长度为k的子串数。
/*************************************************
*** zoj 3587 拓展kmp的定义题:求出S的后缀子串i
与T的最长前缀k,实际上表示已si
开头的子串段长度<=k,均有一种方案!
同理,找到S反串与T的反串的最长前缀!
*** 实际运用ex_kmp求出与T的前缀长度为k的子串的个数!
*************************************************/
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <math.h>
#define llt long long
#include <map>
#define fi first
#define se second
#define lson (rt<<1)
#define rson ((rt<<1)+1)
using namespace std;
const int N = 1e5+777;
char s[N],t[N];
char ss[N],tt[N];//反串
llt sum[N],a[N];//a 记录 S的子串是T的长度为k的后缀子串的个数
// sum 记录 S的子串是T的长度不超过k的后缀子串的个数和
int Next[N],extend[N];
void getNext(char S[],int n){
Next[0] = n;
int i =0 ;
while(i+1<n&&S[i]==S[i+1]) ++i;
Next[1] = i;
int po = 1;
for(int i=2;i<n;++i){
if(i+Next[i-po]<po+Next[po])
Next[i] = Next[i-po];
else {
int j = max(0,po+Next[po]-i);
while(i+j<n&&S[j]==S[i+j]) ++j;
Next[i] = j;
po = i;
}
}
}
void getExtend(char S[],char T[],int slen,int tlen){
getNext(T,tlen);
int i=0;
int mixn = min(slen,tlen);
while(i<mixn&&S[i]==T[i]) ++i;
extend[0] = i;
int po = 0;
for(int i=1;i<slen;++i){
if(i+Next[i-po]<po+extend[po])
extend[i] = Next[i-po];
else {
int j = max(0,po+extend[po]-i);
while(j<tlen&&i+j<slen&&T[j]==S[i+j]) ++j;
extend[i] = j;
po = i;
}
}
}
int main(){
int T;
scanf("%d",&T);
while(T--){
scanf("%s",s);
scanf("%s",t);
int slen = strlen(s);
int tlen = strlen(t);
for(int i=0;i<slen;++i) ss[slen-1-i] = s[i];
for(int i=0;i<tlen;++i) tt[tlen-1-i] = t[i];
/*统计S的子串为T的后缀子串的长度为k的个数*/
memset(a,0,sizeof a);
getExtend(ss,tt,slen,tlen);
for(int i=0;i<slen;++i)
++a[extend[i]];
for(int i=tlen;i>0;--i)
a[i-1] += a[i];
sum[0] = 0;
for(int i=1;i<=tlen;++i)
sum[i] = sum[i-1] + a[i];
/*对于S的第i后缀子串与T的最长公共前缀长度为k,即1-k的前缀子串,
我们统计tlen-k -- tlen-1的与T的后缀子串相同的个数*/
llt ans = 0;
getExtend(s,t,slen,tlen);
for(int i=0;i<slen;++i){
if(extend[i]==0) continue;
if(extend[i]==tlen) extend[i] = tlen-1;
//cout<<extend[i]<<endl;
ans += sum[tlen-1] - sum[tlen-extend[i]-1];
}
printf("%lld\n",ans);
}
}
hdu 4763
题意:问子串S = A**A**A,问A最大长度。
涉及本串的后缀子串为前缀子串|T|,另外一个后缀子串与本串的最长公共子串的长度不小于|T|,显然是ex_kmp的定义题。
我们仅需要预处理:pre数组,pre[i] 维护的是后缀长度不大于i的子串中与本串的前缀交是本身的最大后缀子串的长度。 然后遍历中间的那个A,因为不能有交部分,遍历到i时,前面部分的最长为i,中间A的最长为Next[i] ,取mixn=min(i,Next[i]).后面的A长度不能超过(n-i)/2(AA),取mixn = min(mixn,(n-i)/2).前面的A长度均保证不低于mixn,pre[mixn]即为答案!
/*************************************************
*** hdu 4763 ex_kmp A***A***A问A子串的最大长度
*** 后缀子串与本串的最长公共前缀子串长度问题。
*************************************************/
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <algorithm>
#include <math.h>
#include <map>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#define llt long long
using namespace std;
const int N = 1e6+777;
char s[N];
int Next[N],pre[N];
void get_Next(int n){
Next[0] = n;
int i = 0;
while(i+1<n&&s[i]==s[i+1]) ++i;
Next[1] = i;
int po = 1;
for(int i=2;i<n;++i){
if(i-po+Next[i-po]<Next[po])
Next[i] = Next[i-po];
else {
int j = max(0,po+Next[po]-i);
while(i+j<n&&s[j]==s[i+j])++j;
Next[i] = j;
po = i;
}
}
}
int main(){
int T;
scanf("%d",&T);
while(T--){
scanf("%s",s);
int n = strlen(s);
get_Next(n);
/*提前统计后缀子串长度不超过i的子串中也是一个前缀子串的最大长度*/
pre[0] = 0;
for(int i=1;i<=n;++i)
pre[i] = max(pre[i-1],Next[n-i]);
int ans = 0;
for(int i=1;i<n-1;++i){
int mixn = min(i,Next[i]);
mixn = min(mixn,(n-i)/2);
ans = max(ans,pre[mixn]);
}
printf("%d\n",ans);
}
return 0;
}
二、回文前缀子串和回文后缀子串方面应用
hdu 3613
题意: 每个字母都有权值,将字符串拆成前后两个字符串,如果拆分后的字符串是回文串,统计其字母权值和,如果不是回文串,和为0,问拆分两个字符串的权值最大和。
ex_kmp的一个应用:判断前缀、后缀字符串是否为回文串。
回文串正读和反读必须相同,则制造反串S1,求出原串的所有后缀子串S[i……n-1]与S1的最长公共前缀长度,如果长度等于后缀子串的长度,那么证明该后缀子串正反读相同,即此后缀子串为一个后缀回文子串。
同理,求出反串的所有后缀子串S1[i,n-1](即为前缀反读)与原串S的最长公共前缀长度,如果长度等于反串的后缀子串的长度,那么证明该前缀为前缀回文子串。
/*************************************************
*** hdu 3613 ex_kmp 求前缀子串回文 后缀子串回文
*************************************************/
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <algorithm>
#include <math.h>
#include <map>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#define llt long long
using namespace std;
const int N = 5e5+7777;
char s[2][N];
int Next[N],extend[2][N];
// 一维为0即表示本串为模式串,为1即表示反串为模式串
int w[30];//记录字母的权值
int sum[N];//前i项权值和
void get_Next(char S[],int n){
Next[0] = n;
int i = 0;
while(i+1<n&&S[i]==S[i+1]) ++i;
Next[1] = i;
int po = 1;
for(int i=2;i<n;++i){
if(i+Next[i-po]<po+Next[po])
Next[i] = Next[po];
else{
int j =max(0,po+Next[po]-i);
while(i+j<n&&S[j]==S[i+j]) ++j;
Next[i] = j;
po = i;
}
}
}
// 模式串为S 匹配串为T
// 匹配T的所有后缀子串与S的最长公共前缀长度
void extend_KMP(char S[],char T[],int n,int ex[]){
get_Next(S,n);
int i = 0;
while(i<n&&S[i]==T[i]) ++i;
ex[0] = i;
int po = 0;//当前匹配的最远位置的点
for(int i=1;i<n;++i){
if(i+Next[i-po]<po+ex[po])
ex[i] = Next[i-po];
else{
int j = max(0,po+ex[po]-i);
while(i+j<n&&S[j]==T[i+j]) ++j;
ex[i] = j;
po = i;
}
}
}
int main(){
int T;
scanf("%d",&T);
while(T--){
for(int i=1;i<=26;++i)
scanf("%d",&w[i]);
scanf("%s",s[0]);
int n = strlen(s[0]);
/*构造反串*/
for(int i=0;i<n;++i)
s[1][n-1-i] = s[0][i];
/*正串为模式串,反串为匹配串,得到前缀回文串*/
extend_KMP(s[0],s[1],n,extend[0]);
/*反串为模式串,正串为匹配串,得到后缀回文串*/
extend_KMP(s[1],s[0],n,extend[1]);
sum[0] = 0;
for(int i=1;i<=n;++i)
sum[i] = sum[i-1] + w[s[0][i-1]-'a'+1];
int ans = -1e9;
for(int i=1;i<n;++i){
int tmp = 0;
/*前缀[0,i-1]为回文串*/
if(extend[0][n-i]==i) tmp += sum[i];
/*后缀[i,n-1]为回文串*/
if(extend[1][i]==n-i) tmp += sum[n] - sum[i];
// cout<<extend[0][n-i]<<" "<<extend[1][i]<<endl;
ans = max(ans,tmp);
}
printf("%d\n",ans);
}
return 0;
}
/*****************
5555
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
abaa
*/
SPOJ - VPALIN
题意:求n个字符串,两两前后拼接成回文串的对数。
01字典树 + ex_kmp 处理 字符串的前缀、后缀回文串
扫描二维码关注公众号,回复: 4166854 查看本文章
/**************************************
*** hdu 3294 Manacher算法
**************************************/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <math.h>
#include <map>
#include <cstdlib>
#define llt long long
#define lson (rt<<1)
#define rson ((rt<<1)+1)
using namespace std;
const int N = 2e6+777;
char s[N],t[N];
int nxt[N],ext[2][N];
struct node{
int id;
int len;
bool operator <(const node &a)const{
return len<a.len;
}
}p[N];
struct tried{
int val;//记录以此节点为末尾的单词个数
int son[26];
}q[N*2];
int toUsed = 2;
void Insert(char A[],int loc,int len){// loc 指1 表示正序字典树 loc指2 表示反序字典树
for(int i=0;i<len;++i){
int sn = A[i]-'a';
if(!q[loc].son[sn]) q[loc].son[sn] = ++toUsed;
loc = q[loc].son[sn];
}
++q[loc].val;
}
llt query(char A[],int loc,int id,int len){// id为1 表示正串跑反序字典树
llt ans =0;
for(int i=0;i<len;++i){
int sn = A[i]-'a';
if(!q[loc].son[sn]) break;
loc = q[loc].son[sn];
if(q[loc].val!=0&&ext[id][i+1]+i+1==len) ans += q[loc].val;
}
return ans;
}
void get_nxt(char *S,int n){
nxt[0] = n;
int i = 0;
while(i+1<n&&S[i]==S[i+1])++i;
nxt[1] = i;
int po = 1;
for(int i=2;i<n;++i){
if(i+nxt[i-po]<po+nxt[po])
nxt[i] = nxt[i-po];
else {
int j = max(0,po+nxt[po]-i);
while(i+j<n&&S[j]==S[i+j])++j;
nxt[i] = j;
po = i;
}
}
}
void get_ext(char *S,char *T,int n,int id){
// for(int i=0;i<n;++i)
// cout<<S[i];
//cout<<endl;
// for(int i=0;i<n;++i)
// cout<<T[i];
// cout<<endl;
get_nxt(T,n);
int i = 0;
while(i<n&&S[i]==T[i])++i;
ext[id][0] = i;
int po = 0;
for(int i=1;i<n;++i){
if(i+nxt[i-po]<po+ext[id][po])
ext[id][i] = nxt[i-po];
else{
int j = max(0,po+ext[id][po]-i);
while(i+j<n&&T[j]==S[i+j]) ++j;
ext[id][i] = j;
po = i;
}
}
}
int main(){
int n;
while(~scanf("%d",&n)){
int tot = 0;
for(int i=0;i<n;++i){
p[i].id = tot;
scanf("%d%s",&p[i].len,s+tot);
tot += p[i].len;
}
for(int i=0;i<tot;++i)
t[tot-1-i] = s[i];
sort(p,p+n);
llt ans = 0;
for(int i=0;i<n;++i){
int tmp = tot - (p[i].len+p[i].id);
//cout<<tmp<<endl;
get_ext(s+p[i].id,t+tmp,p[i].len,1);
get_ext(t+tmp,s+p[i].id,p[i].len,0);
if(ext[1][0]==p[i].len) ++ans;
//cout<<ans<<" ";
ans += query(s+p[i].id,2,1,p[i].len);
//cout<<ans<<" ";
ans += query(t+tmp,1,0,p[i].len);
//cout<<ans<<endl;
Insert(s+p[i].id,1,p[i].len);
Insert(t+tmp,2,p[i].len);
}
printf("%lld\n",ans);
}
return 0;
}
/****************
3
2 aa
2 aa
2 aa
**********/
hdu 2609
题意:给n个01等长字符串,所有本串循环同构串,等价为相同字符串,问n个字符串中不同的字符串个数。
hash 做法: 在线对每个字符串枚举其所有循环同构串,在map中查找是否之前出现过,如果其中有一个出现过,这个字符串与之前相同!如果未出现过,插入本串即可!
/*************************************************
*** hdu 2609 hash做法
*************************************************/
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <math.h>
#define llt long long
#include <map>
#define fi first
#define se second
#define lson (rt<<1)
#define rson ((rt<<1)+1)
using namespace std;
const int N = 1e6+777;
char s[N*2];
struct strCmp{
bool operator ()(char const *a,char const *b)const{
return strcmp(a,b)<0;
}
};
map<char*,int,strCmp> Hash;
int main(){
int n;
while(~scanf("%d",&n)){
Hash.clear();
int ans = 0;
int t = 0;
for(int i=0;i<n;++i){
scanf("%s",s+t);
int len = strlen(s+t);
int flag = 1;
for(int i=0;i<len&&flag;++i){
s[i+len+t] = s[i+t];
//s[i+len+1+t]='\0';
map<char*,int,strCmp>::iterator it = Hash.find(s+i+1+t);
if(it!=Hash.end()){flag=0;break;}
}
//cout<<endl;
s[len+t]='\0';
// cout<<s<<endl;
if(flag) {++ans;Hash[s+t] = 1;}
t += 2*len;
}
memset(s,0,sizeof s);
printf("%d\n",ans);
}
}
三、分治 + 拓展KMP
1、最长回文子串的长度
/**************************************
*** poj 3974 ex_kmp 处理 最长回文子串
时间复杂度 O(nlog n) 还是 O(n^2)
**************************************/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <math.h>
#include <map>
#include <cstdlib>
#define llt long long
#define lson (rt<<1)
#define rson ((rt<<1)+1)
using namespace std;
typedef long long ll;
const int N = 1e6+777;
char s[N],ss[N];
int nxt[N],ext1[N],ext2[N];
void get_next(char *S,int n){
nxt[0] = n;
int i = 0;
while(i+1<n&&S[i]==S[i+1]) ++i;
nxt[1] = i;
int po = 1;
for(int i=2;i<n;++i){
if(i+nxt[i-po]<po+nxt[po])
nxt[i] = nxt[i-po];
else{
int j = max(0,po+nxt[po]-i);
while(i+j<n&&S[j]==S[i+j])++j;
nxt[i] = j;
po = i;
}
}
}
void get_ext(char *S,char *T,int *ext,int n,int m){
get_next(T,m);
int i = 0;
while(i<n&&i<m&&S[i]==T[i])++i;
ext[0] = i;
int po = 0;
for(int i=1;i<n;++i){
if(i+nxt[i-po]<po+ext[po])
ext[i] = nxt[i-po];
else {
int j = max(0,po+ext[po]-i);
while(j<m&&i+j<n&&T[j]==S[i+j]) ++j;
ext[i] = j;
po = i;
}
}
}
int ans = 0;
/*求出S的回文后缀子串 */
/*左部反向的后缀子串与右部的最长前缀长度*/
void solve(char *S,char *SS,char *T,int n,int m){
get_ext(S,SS,ext1,n,n);
get_ext(SS,T,ext2,n,m);
ans = max(ans,2*ext2[0]);
for(int i=1;i<n;++i){
int len = n-i;
if(ext1[i]!=len||ext2[len]==0) continue;
ans = max(ans,ext1[i]+2*ext2[len]);
}
}
void Find(int l,int r,int n){
if(l==r) return ;
int mid = (l+r)>>1;
solve(s+l,ss+n+1-mid,s+mid+1,mid-l+1,r-mid);
solve(ss+n+1-r,s+mid+1,ss+n+1-mid,r-mid,mid-l+1);
//cout<<l<<" "<<r<<" "<<ans<<endl;
if(ans<mid-l+1)Find(l,mid,n);
if(ans<r-mid) Find(mid+1,r,n);
}
int main(){
int cas = 0;
while(~scanf("%s",s+1)&&s[1]!='E'){
int n = strlen(s+1);
for(int i=1;i<=n;++i) ss[n+1-i] = s[i];
ans = 1;
Find(1,n,n);
printf("Case %d: %d\n",++cas,ans);
}
return 0;
}
2、统计不重叠重复次数为2的子串长度为k的个数
hunnu oj 11565
题意:q次询问,每次问长度为k的不重叠重复次数为2的连续序列的个数。
这题有点可惜,是道非常好的题!但标程错了!
标程将所有整数强制转换为char,如果为负数,都变成了’?’,太可悲了!
我wa了两天!
/***************************************************************
*** hunnu oj11565 分治+ex_kmp统计不同长度的不重叠重复次数子串的个数
****************************************************************/
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <math.h>
#define llt long long
#define uint unsigned int
#include <map>
#define fi first
#define se second
#define lson (rt<<1)
#define rson ((rt<<1)+1)
using namespace std;
const int N = 5e5+777;
int n,q;
int a[N],aa[N];
int sum[N]={0};
int nxt[N];
int extend[N];
void getNext(int *S,int n){
// fill(nxt,nxt+n,0);
nxt[0] = n;
int i = 0;
while(i+1<n&&S[i]==S[i+1])++i;
nxt[1] = i;
int po = 1;
for(int i=2;i<n;++i){
if(i+nxt[i-po]<po+nxt[po])
nxt[i] = nxt[i-po];
else {
int j = max(0,po+nxt[po]-i);
while(i+j<n&&S[j]==S[i+j]) ++j;
nxt[i] = j;
po = i;
}
}
}
void getExtend(int *S,int *T,int n,int m){
getNext(T,m);
int mixn = min(n,m);
int i = 0;
while(i<mixn&&S[i]==T[i])++i;
extend[0] = i;
int po = 0;
for(int i=1;i<n;++i){
if(i+nxt[i-po]<po+extend[po])
extend[i] = nxt[i-po];
else {
int j = max(0,po+extend[po]-i);
while(j<m&&i+j<n&&T[j]==S[i+j])++j;//不能小于mixn
extend[i] = j;
po = i;
}
}
}
void solve(int *S,int *T,int l,int mid,int r,int sgn){
/*统计跨过mid的nice序列*/
getExtend(S+l,S+mid+1,mid-l+1,r-mid);
getNext(T+n+1-mid,mid-l+1);
/*以a-mid 、 a-(mid+1)为对称的个数*/
if(sgn==1)
for(int i=l;i<=mid;++i)
if(extend[i-l]==mid+1-i) ++sum[2*(mid-i+1)];
/*左部居多*/
for(int i=l+1;i<=mid;++i){
int len = mid+1-i;
int s1 = min(extend[i-l],len-1);
int s2 = min(nxt[len],len-1);
if(s1+s2<len||s1==0||s2==0) continue;
sum[2*len] += 1+s1+s2-len;
}
}
void find(int l,int r){//统计区间[l,r]内nice序列的个数
if(l==r) return;
int mid = (l+r)>>1;
solve(a,aa,l,mid,r,1);
solve(aa,a,n+1-r,n-mid,n+1-l,0);
find(l,mid);
find(mid+1,r);
}
int main(){
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
while(~scanf("%d%d",&n,&q)){
memset(sum,0,sizeof sum);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
aa[n-i+1]=a[i];
}
find(1,n);
while(q--){
int x;
scanf("%d",&x);
printf("%d\n",sum[x]);
}
}
return 0;
}
/*
9 4
1 1 -1 1 1 -1 1 1 -1
2 4 6 8
*/