1. 程式人生 > >串的模式匹配演算法---BF、KMP

串的模式匹配演算法---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>s[i]=t[j],如果對應位置相等,將i,j後移,繼續比較後面的字元。

<2>s[i]\neq t[j],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; 
}