串的模式匹配演算法---BF、KMP
尋找字串S中字串T出現的位置或者次數的問題屬於字串匹配問題。
BF演算法:
eg:
主串:s="ababcabcacbab";
模式串:t="abc";
1.變數i,j(初始值為0、1都行)分別指向S、T的第一個位置(這裡是指i=1;j=1(i=0;j=0))。
2.對於每一個字母依次進行比較(i<=s.size&&<=t.size(i<s.length&&<t.length)):
<1>,如果對應位置相等,將i,j後移,繼續比較後面的字元。
<2>,i,j後退重新匹配,從主串的下一個字元(i-j+2)重新與模式串的第一個字元(j=1)比較。
3.如果j>t.length,說明匹配成功。返回i-t.length
演算法模板:
int i=1;j=1; int s=strlen(a); int t=strlen(b); while(i<=s&&j<=t){ if(a[i]==b[i]){//相等:i,j後移,繼續比較後續字元 i++;j++; }else{//不相等 i=i-j+2;//i指向主串的下一個字元 j=1;//j從模式串的第一個字元開始比較 } if(j>t) return i-t; return -1;//返回-1表示未找到 }
AC程式碼:
#include<stdio.h> #include<iostream> #include<string.h> #include<stdlib.h> #include<string> #include<map> #include<algorithm> using namespace std; const int Max_n=1001; char a[Max_n],b[Max_n]; int main(){ while(~scanf("%s%s",a,b)){ int sum=0; if(a[0]=='#') break; int s=strlen(a); int t=strlen(b); int i=0,j=0;//這裡注意,我是從下標為0開始的 while(i<s&&j<t){ if(a[i]==b[j]){ i++; j++; } else{ i=i-j+1;//注意下標為0開始和下標為1開始的區別 j=0; } if(j>=t){//如果匹配成功 j=0;//j再次從0開始,開始一輪新的匹配,i此時就是成功後最後一個字元的下一個字元 sum++;//出現次數+1; if(i>=s)//i超過主串的長度就跳出 break; } } printf("%d\n",sum); } return 0; }
KMP演算法:
next[]陣列含義:
<1>.當主串中的第i個字元與模式串中的第j個字元"失配"(不相等)時,主串中第i個字元(此時i指向的位置不動)應該與模式串中的哪個字元在比較。
<2>.模式串下標x之前的最長等值前後綴的長度(下標從0開始)
模式串下標x之前的最長等值前後綴的長度+1(下標從1開始)
next[]計算值證明略。
kmp、next[]---模板
//下標從1開始(從0開始)
void f(int s){
net[1]=0;//1號位置為0 (net[0]=-1)
for(int i=1,j=0;i<s;){ //(for(int i=0,j=-1;i<s;))
if(j==0||b[i]==b[j]){//j==0,或者此時匹配 //(if(j==-1||b[i]==b[j]))
i++;
j++;
net[i]=j;//i位置就是下標i之前的最長等值前後綴的長度+1
}else{
j=net[j];//如果不匹配,j就是i之前長等值前後綴的最後一個字元,
也就是下標i之前的最長等值前後綴的長度。
}
}
}
int kmp(int s,int t){
int i=1,j=1;
while(i<=s&&j<=t){
if(j==0||a[i]==b[j]){//j=0或者此時匹配,繼續下一個位置 (j==-1||a[i]==b[j])
i++;
j++;
}else{
j=net[j];//否則,主串中i位置的字元應該與net[j]比較
}
}
if(j>t)//匹配成功
return i-t;
return -1;
}
nextval陣列含義:.當主串中的第i個字元與模式串中的第j個字元"失配"(不相等)時,主串中第i個字元(此時i指向的位置不動)應該與模式串中的哪個字元在比較。計算原理略。
模板:
void f(int t){
nextval[1]=0;//1號位置為0
for(int i=1,j=0;i<t;){
if(j==0||b[i]==b[j]){//如果匹配
i++;
j++;
if(b[i]!=b[j]){//net[i]那個位置(也就是j)與當前位置不等
nextval[i]=j;//nextval[i]的值就是net[j]位置(也就是j)上面的值
}else{//相等
nextval[i]=nextval[j];//nextval[i]此時的值是j(net[j])
//位置上面的值也就是nextval[j]
}
}else{
j=nextval[j];
}
}
}
AC程式碼:
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<map>
#include<algorithm>
#define inf 0x3f3f3f3f
using namespace std;
const int Max_a=1000010;
const int Max_b=10010;
int a[Max_a],b[Max_b],nextval[Max_b];
void f(int t){
nextval[1]=0;
for(int i=1,j=0;i<t;){
if(j==0||b[i]==b[j]){
i++;
j++;
if(b[i]!=b[j]){
nextval[i]=j;
}else{
nextval[i]=nextval[j];
}
}else{
j=nextval[j];
}
}
}
//void f(int t){
// net[1]=0;
// for(int i=1,j=0;i<t;){
// if(j==0||b[i]==b[j]){
// i++;
// j++;
// net[i]=j;
// }else{
// j=net[j];
// }
// }
//}
int kmp(int s,int t){
int i=1,j=1;
while(i<=s&&j<=t){
if(j==0||a[i]==b[j]){
i++;
j++;
}else{
j=nextval[j];
}
}
if(j>t)
return i-t;
return -1;
}
int main(){
int t;
scanf("%d",&t);
while(t--){
int s,t;
scanf("%d%d",&s,&t);
for(int i=1;i<=s;i++)
scanf("%d",&a[i]);
for(int i=1;i<=t;i++)
scanf("%d",&b[i]);
f(t);
printf("%d\n",kmp(s,t));
}
return 0;
}
AC程式碼:
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<map>
#include<algorithm>
#define inf 0x3f3f3f3f
using namespace std;
const int Max_a=1000010;
const int Max_b=10010;
char a[Max_a],b[Max_b];
int net[Max_b];
//void f(int t){
// nextval[1]=0;
// for(int i=1,j=0;i<t;){
// if(j==0||b[i]==b[j]){
// i++;
// j++;
// if(b[i]!=b[j]){
// nextval[i]=j;
// }else{
// nextval[i]=nextval[j];
// }
// }else{
// j=nextval[j];
// }
// }
//}
void f(int t){
net[1]=0;
for(int i=1,j=0;i<=t;){
if(j==0||b[i]==b[j]){
i++;
j++;
net[i]=j;
}else{
j=net[j];
}
}
}
int kmp(int s,int t){//此題為統計字串出現的次數
int i=1,j=1,sum=0;
while(i<=s&&j<=t){
if(j==0||a[i]==b[j]){
i++;
j++;
}else{
j=net[j];
}
if(j>t){//如果匹配成功
j=net[j];//和下一次應該比較的字元繼續比較
sum++;//出現次數+1
}
}
return sum;
}
int main(){
int n;
scanf("%d",&n);
while(n--){
scanf("%s%s",b+1,a+1);
a[0]=b[0]='0';
int s=strlen(a);
int t=strlen(b);
f(t-1);
printf("%d\n",kmp(s-1,t-1));
}
return 0;
}
AC程式碼:
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<map>
#include<algorithm>
#define inf 0x3f3f3f3f
using namespace std;
const int Max_a=1005;
const int Max_b=1005;
char a[Max_a],b[Max_b];
int net[Max_b];
//void f(int t){
// nextval[1]=0;
// for(int i=1,j=0;i<t;){
// if(j==0||b[i]==b[j]){
// i++;
// j++;
// if(b[i]!=b[j]){
// nextval[i]=j;
// }else{
// nextval[i]=nextval[j];
// }
// }else{
// j=nextval[j];
// }
// }
//}
void f(int t){
net[1]=0;
for(int i=1,j=0;i<=t;){
if(j==0||b[i]==b[j]){
i++;
j++;
net[i]=j;
}else{
j=net[j];
}
}
}
int kmp(int s,int t){//與上題不同的是,一次匹配結束後,模式串從頭開始再次匹配
int i=1,j=1,sum=0;
while(i<=s&&j<=t){
if(j==0||a[i]==b[j]){
i++;
j++;
}else{
j=net[j];
}
if(j>t){
j=1;//由於上述原因,此處j從頭開始
sum++;
}
}
return sum;
}
int main(){
while(~scanf("%s%s",a+1,b+1)&&a[1]!='#'){
a[0]=b[0]='0';
int s=strlen(a);
int t=strlen(b);
f(t-1);
printf("%d\n",kmp(s-1,t-1));
}
return 0;
}
此題題意是說一個字串裡如果需要把它補成>=2個相同的字串需要多少個字元。
aaa 重複的a出現了>=2次,不需要新增字元 0
abca 須補成abcabc 2
abcde 須補成abcdeabcde 2
我們只需要求出重複出現的字串的長度即可(最小迴圈節)
AC程式碼:
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<map>
#include<algorithm>
#define inf 0x3f3f3f3f
using namespace std;
const int Max_a=100005;
char a[Max_a];
int net[Max_a];
void f(int t){
net[0]=-1;
for(int i=0,j=-1;i<t;){
if(j==-1||a[i]==a[j]){
i++;
j++;
net[i]=j;
}else{
j=net[j];
}
}
}
int main(){
int n;
scanf("%d",&n);
while(n--){
memset(net,0,sizeof(net));
scanf("%s",a);
int t=strlen(a);
f(t);
int l=t-net[t];//最小迴圈節的長度
if(t%l==0){//字元長度剛好是最小迴圈節的整數倍
if(l==t)//最小迴圈節的長度=字串的長度 ab
printf("%d\n",l);
else//一個字元,或者最小迴圈節出現了整數個(>=2)
printf("0\n");
}else{//不是整數倍
printf("%d\n",l-t%l);//最小迴圈節的長度-不是最小迴圈節那部分多出來字元的個數
}
}
return 0;
}
題意:給你一個字串,某個位置及其之前的字串如果剛好是最小迴圈節的整數倍(>1),就輸出此時的位置及最小迴圈節的倍數。
AC程式碼:
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<map>
#include<algorithm>
#define inf 0x3f3f3f3f
using namespace std;
const int Max_a=1000005;
char a[Max_a];
int net[Max_a];
void f(int t){
net[0]=-1;
for(int i=0,j=-1;i<t;){
if(j==-1||a[i]==a[j]){
i++;
j++;
net[i]=j;
}else{
j=net[j];
}
}
}
int main(){
int n,m=0;
while(~scanf("%d",&n)&&n){
memset(net,0,sizeof(net));
scanf("%s",a);
f(n);
printf("Test case #%d\n",++m);
for(int i=2;i<=n;i++){//根據題意迴圈直接從2開始
int l=i-net[i];//求出當前字首的最小迴圈節
if(i%l==0&&(i/l>1))//如果是當前長度的整數倍&&(>1)j即可輸出
printf("%d %d\n",i,i/l);
}
printf("\n");//控制兩個示例之間的空格
}
return 0;
}
題意:給你一個字串,找出一個子串,使得這個字串既是這個字串的字首,也是這個字串的字尾。
首先是一個整個字串區間,他的最長前後綴找到以後肯定是滿足條件的一個字串,我們在用next陣列,將j滑到最長字首的最後一個字元的位置,我們一直重複這樣的過程,直到到第一個位置為止。
AC程式碼:
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<map>
#include<algorithm>
#define inf 0x3f3f3f3f
using namespace std;
const int Max_a=400005;
char a[Max_a];
int net[Max_a],b[Max_a];
void f(int t){
net[0]=-1;
for(int i=0,j=-1;i<t;){
if(j==-1||a[i]==a[j]){
i++;
j++;
net[i]=j;
}else{
j=net[j];
}
}
}
int main(){
while(~scanf("%s",a)){
memset(net,0,sizeof(net));
memset(b,0,sizeof(b));
int n=strlen(a);
f(n);
int j=n;
int i=1;
while(j!=0){//第一個位置是迴圈出口
b[i++]=j;//用陣列來存放要輸出的位置
j=net[j];//j需要一直滑動
}
for(int j=i-1;j>1;j--)
printf("%d ",b[j]);
printf("%d\n",b[1]);
}
return 0;
}
題意:找第一個字串的字首和第二個字串的字尾一樣的最長長度
AC程式碼:
解法一:我們把兩個字串拼接求其最長前後綴的長度,但是這裡可能會超過第一個字串的長度,或者大於第二個字元的長度,所以需要遞迴,直到長度<=第一個字串的長度(第二個字串的長度)。
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<map>
#include<algorithm>
#define inf 0x3f3f3f3f
using namespace std;
const int Max_a=100005;
char a[Max_a],b[Max_a];//這裡注意陣列長度,因為要拼接,剛開始就是因為這個程式一直超時
int net[Max_a];
void f(int t){
net[0]=-1;
for(int i=0,j=-1;i<t;){
if(j==-1||a[i]==a[j]){
i++;
j++;
net[i]=j;
}else{
j=net[j];
}
}
}
int f(int j,int mmin){
if(j>mmin)//如果當前所求長度大於字串最小長度
return f(net[j],mmin);//j滑向下一個位置再次重複上面的過程
return j;//最後返回滿足題意的長度
}
int main(){
while(~scanf("%s%s",a,b)){
memset(net,0,sizeof(net));
int a1=strlen(a);
int b1=strlen(b);
for(int i=a1,j=0;j<b1;i++,j++)
a[i]=b[j];//拼接字串
int n=a1+b1;
f(n);
int j=net[n];//前後綴匹配最長的字串長度
int mmin=min(a1,b1);//兩個字串長度的最小值
int mmax=f(j,mmin);
if(mmax!=0){//如果能找到輸出字元和長度
a[mmax]='\0';//這裡只是為了方便輸出
printf("%s %d\n",a,mmax);
}
else//否則輸出0
printf("0\n");
}
return 0;
}
解法二:按照kmp來做
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<map>
#include<algorithm>
#define inf 0x3f3f3f3f
using namespace std;
const int Max_a=50005;
char a[Max_a],b[Max_a];
int net[Max_a];
void f(int t){
net[0]=-1;
for(int i=0,j=-1;i<t;){
if(j==-1||a[i]==a[j]){
i++;
j++;
net[i]=j;
}else{
j=net[j];
}
}
}
int kmp(int s,int t){
int i=0,j=0;
while(i<s&&j<t){
if(j==-1||b[i]==a[j]){
i++;j++;
}else{
j=net[j];
}
if(j>=t&&i!=s)//當匹配成功的時候,如果此時的i沒有到最後一個字元的下一個位置
j=net[j];//主串繼續與模式串的net[j]位置上面的字元繼續比較
}
//最後返回i匹配到最後j的位置(即能夠匹配的最大長度)
return j;
}
int main(){
while(~scanf("%s%s",a,b)){//a為模式串,b為主串
memset(net,0,sizeof(net));//此處必須初始化net陣列
int a1=strlen(a);
int b1=strlen(b);
f(a1);
int mmax=kmp(b1,a1);//讓兩個字串進行模式匹配
a[mmax]='\0';//這裡為了方便輸出
if(mmax==0)//未匹配成功
printf("0\n");
else
printf("%s %d\n",a,mmax);
}
return 0;
}