字符串匹配的意思是給一個字符串集合,和另一個字符串集合,看這兩個集合交集是多少。
若是都只有一個字符串,那麼就看其中一個是否包含另外一個;
若是父串集合(比較長的,被當做模板)的有多個,子串(拿去匹配的)只有一個,就是問這個子串是否存在於父串之中;
若是子串父串集合都有多個,那麼就是問交集了。
1.KMP算法
KMP算法是用來處理一對一的匹配的。
樸素的匹配算法,或者說暴力匹配法,就是將兩個字符串從頭比到尾,若是有一個不同,那麼從下一位再開始比。這樣太慢了。所以KMP算法的思想是,對匹配串本身先做一個處理,得到一個next數組。這個數組是做什麼用的呢?next [j] = k,代表j之前的字符串中有最大長度爲k 的相同前綴後綴。記錄這個有什麼用呢?對於ABCDABC這個串,如果我們匹配ABCDABTBCDABC這個長串,當匹配到第7個字符T的時候就不匹配了,我們就不用直接移到B開始再比一次,而是直接移到第5位來比較,豈不美哉?所以求出了next數組,KMP就完成了一大半。next數組也可以說是開始比較的位數。
計算next數組的方法是對於長度爲n的匹配串,從0到n-1位依次求出前綴後綴最大匹配長度。
比如ABCDABD這個串:
(圖片來源https://www.cnblogs.com/zhangtianq/p/5839909.html)
如何去求next數組呢?k是匹配下標。這裏沒有從最後一位開始和第一位開始分別比較前綴後綴,而是利用了next[i-1]的結果。
void getnext()//獲取next數組
{
int i,n,k;
n=strlen(ptr);
memset(next,0,sizeof(next));
k=0;
for(i=1;i<n;i++)
{
while(k>0 && ptr[k]!=ptr[i])
k=next[k];
if(ptr[k]==ptr[i]) k++;
next[i+1]=k;
//next表示的是匹配長度
}
}
這裏我是按照《算法導論》的代碼來寫的。算法導論算法循環是從1到n而不是從0到n-1,所以在下面匹配的時候需要j=next[j+1]。
int kmp(char *a,char *b)//匹配ab兩串,a爲父串
{
int i=0,j=0;
int len1=strlen(a);
int len2=strlen(b);
getnext();
while(i<len1&&j<len2)
{
if(j==0||a[i]==b[j])
{ i++;j++; }
else j=next[j+1];//到前一個匹配點
}
if(j>=len2)
return i-j;
else return -1;
}
這裏next數組的作用就顯現出來了。最後返回的是i-j,也就是說,是從i位置前面的第j位開始的,也就是上面說的,next數組也可以說是開始比較的位數。也就是說,在父串的i位比的時候已經是在比子串的第j位了。
一個完整的代碼:
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
const int N=100;
char str[100],ptr[100];//父串str和子串ptr
int next[100];
string ans;
void getnext()//獲取next數組
{
int i,n,k;
n=strlen(ptr);
memset(next,0,sizeof(next));
k=0;
for(i=1;i<n;i++)
{
while(k>0 && ptr[k]!=ptr[i])
k=next[k];
if(ptr[k]==ptr[i]) k++;
next[i+1]=k;
//next表示的是匹配長度
}
}
int kmp(char *a,char *b)//匹配ab兩串,a爲父串
{
int i=0,j=0;
int len1=strlen(a);
int len2=strlen(b);
getnext();
while(i<len1&&j<len2)
{
if(j==0||a[i]==b[j])
{ i++;j++; }
else j=next[j+1];//到前一個匹配點
}
if(j>=len2)
return i-j;
else return -1;
}
int main(){
while( scanf( "%s%s", str, ptr ) )
{
int ans = kmp(str,ptr);
if(ans>=0)
printf( "%d\n", kmp( str,ptr ));
else
printf("Not find\n");
}
return 0;
}
2.字典樹算法
上面的KMP是一對一匹配的時候常用的算法。而字典樹則是一對多的時候匹配常用算法。其含義是,把一系列的模板串放到一個樹裏面,然後每個節點存的是它自己的字符,從根節點開始往下遍歷就可以得到一個個單詞了。
(圖片來自百度)
我這裏寫的代碼稍微和上面有一點區別,我的節點tnode裏面沒有存它本身的字符,而是存一個孩子數組。所以當數據量很大的時候還是需要做一些變通的,不可直接套用此代碼。若是想以每個節點爲一個node,那麼要注意根節點是空的。
樹的節點tnode,這裏的next[i]存的是子節點指針。sum=0表示這個點不是重點。爲n>0表示有n個單詞以此爲終點。
struct tnode{
int sum;//用來判斷是否是終點的
tnode* next[26];
tnode(){
for(int i =0;i<26;i++)
next[i]=NULL;
sum=0;
}
};
插入函數:
這個newnode是手寫的構造函數.C++類有些坑,不像java那麼...隨便。
假設字典樹已經有了aer,現在插入abc,首先看a,不爲空,那麼直接跳到a節點裏,看b,爲空,那麼新建,跳到b裏,新建c,跳出。
tnode* newnode(){
tnode *p = new tnode;
for(int i =0;i<26;i++)
p->next[i]=NULL;
p->sum=0;
return p;
}
//插入函數
void Insert(char *s)
{
tnode *p = root;
for(int i = 0 ; s[i] ; i++)
{
int x = s[i] - 'a';
if(p->next[x]==NULL)
{
tnode *nn=newnode();
for(int j=0;j<26;j++)
nn->next[j] = NULL;
nn->sum = 0;
p->next[x]=nn;
}
p = p->next[x];
}
p->sum++;//這個單詞終止啦
}
字符串比較:就是一個個字符去比唄...時間複雜度O(m),m是匹配串長度。
bool Compare(char *ch)
{
tnode *p = root;
int len = strlen(ch);
for(int i = 0; i < len; i++)
{
int x = ch[i] - 'a';
p = p->next[x];
if(p==NULL)
return false;
if(i==len-1 && p->sum>0 ){
return true;
}
}
return false;
}
給個完整的代碼:
#include<queue>
#include<set>
#include<cstdio>
#include <iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
/*
trie字典樹
*/
struct tnode{
int sum;//用來判斷是否是終點的
tnode* next[26];
tnode(){
for(int i =0;i<26;i++)
next[i]=NULL;
sum=0;
}
};
tnode *root;
tnode* newnode(){
tnode *p = new tnode;
for(int i =0;i<26;i++)
p->next[i]=NULL;
p->sum=0;
return p;
}
//插入函數
void Insert(char *s)
{
tnode *p = root;
for(int i = 0 ; s[i] ; i++)
{
int x = s[i] - 'a';
if(p->next[x]==NULL)
{
tnode *nn=newnode();
for(int j=0;j<26;j++)
nn->next[j] = NULL;
nn->sum = 0;
p->next[x]=nn;
}
p = p->next[x];
}
p->sum++;//這個單詞終止啦
}
//匹配函數
bool Compare(char *ch)
{
tnode *p = root;
int len = strlen(ch);
for(int i = 0; i < len; i++)
{
int x = ch[i] - 'a';
p = p->next[x];
if(p==NULL)
return false;
if(i==len-1 && p->sum>0 ){
return true;
}
}
return false;
}
void DELETE(tnode * &top){
if(top==NULL)
return;
for(int i =0;i<26;i++)
DELETE(top->next[i]);
delete top;
}
int main()
{
int n,m;
cin>>n;
char s[20];
root = newnode();
for(int i =0;i<n;i++){
scanf("%s",s);
Insert(s);
}
cin>>m;
for(int i =0;i<m;i++){
scanf("%s",s);
if(Compare(s))
cout<<"YES"<<endl;
else
cout<<"NO"<<endl;
}
DELETE(root);//看見指針就要想到釋放,然而這東西會花時間,所以網上很多人寫ACM題就不delete了,我很看不慣這一點。
return 0;
}
3.AC自動機
字典樹是一對多的匹配,那麼AC自動機就是多對多的匹配了。意思是:給一個字典,再給一個m長的文本,問這個文本里出現了字典裏的哪些字。
這個問題可以用n個單詞的n次KMP算法來做(效率爲O(n*m*單詞平均長度)),也可以用1個字典樹去匹配文本串的每個字母位置來做(效率爲O(m*每次字典樹遍歷的平均深度))。上面兩種解法效率都不高,如果用AC自動機來解決的話,效率將爲線性O(m)時間複雜度。
AC自動機也運用了一點KMP算法的思想。簡述爲字典樹+KMP也未爲不可。
首先講一下acnode的結構:
與字典樹相比,就多了個*fail對吧,這個就相當於KMP算法裏的next數組。只不過它存的是失配後跳轉的位置,而不是跳轉之後再向前跳了多少罷了。
struct acnode{
int sum;
acnode* next[26];
acnode* fail;
acnode(){
for(int i =0;i<26;i++)
next[i]=NULL;
fail= NULL;
sum=0;
}
};
插入什麼的我就不說了,記得把fail置爲空即可。
這裏說一下fail指針的獲取。fail指針是通過BFS來求的。
看這麼一張圖
(圖片來自百度)
圖中數字我們不用管它,綠色代表是終點,虛線就是fail指針了。我們可以看到91 E節點的fail指針是指向76 E 的,也就是說執行到這裏如果無法繼續匹配就會跳到76 E那個節點繼續往後匹配。我們可以看到它們前面都是H,也就是說fail指針指向的是父節點相同的同值節點(根節點視爲與任何節點相同)。我們要算的是在一個長文本里面有多少個出現的單詞,這個fail指針就是爲了快速匹配而誕生的。若文本里出現了HISHERS,我們首先匹配了HIS,有通過fail指針跳到85 S從而匹配SHE,再匹配HERS。fail指針跳到哪裏就代表這一點之前的內容已經被匹配了。這樣就避免了再從頭重複判斷的過程。
在函數裏,當前節點的fail指針也會去更新此節點的孩子的fail指針,因爲父節點相同啊~而且因爲它是此節點的fail指針,這兩個節點的父節點也相同啊~所以一路相同過來,就保證fail指向的位置前綴是相同的。
void getfail(){
queue<acnode*> q;
for(int i = 0 ; i < 26 ; i ++ )
{
if(root->next[i]!=NULL){
root->next[i]->fail = root;
q.push(root->next[i]);
}
}
while(!q.empty()){
acnode* tem = q.front();
q.pop();
for(int i = 0;i<26;i++){
if(tem->next[i]!=NULL)
{
acnode *p;
p = tem->fail;
while(p!=NULL){
if(p->next[i]!=NULL){
tem->next[i]->fail = p->next[i];
break;
}
p=p->fail;
}
if(p==NULL)
tem->next[i]->fail = root;
q.push(tem->next[i]);
}
}
}
}
全部代碼如下:
#include<queue>
#include<set>
#include<cstdio>
#include <iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
/*
ac自動機
*/
struct acnode{
int sum;
acnode* next[26];
acnode* fail;
acnode(){
for(int i =0;i<26;i++)
next[i]=NULL;
fail= NULL;
sum=0;
}
};
acnode *root;
int cnt;
acnode* newnode(){
acnode *p = new acnode;
for(int i =0;i<26;i++)
p->next[i]=NULL;
p->fail = NULL;
p->sum=0;
return p;
}
//插入函數
void Insert(char *s)
{
acnode *p = root;
for(int i = 0; s[i]; i++)
{
int x = s[i] - 'a';
if(p->next[x]==NULL)
{
acnode *nn=newnode();
for(int j=0;j<26;j++)
nn->next[j] = NULL;
nn->sum = 0;
nn->fail = NULL;
p->next[x]=nn;
}
p = p->next[x];
}
p->sum++;
}
//獲取fail指針,在插入結束之後使用
void getfail(){
queue<acnode*> q;
for(int i = 0 ; i < 26 ; i ++ )
{
if(root->next[i]!=NULL){
root->next[i]->fail = root;
q.push(root->next[i]);
}
}
while(!q.empty()){
acnode* tem = q.front();
q.pop();
for(int i = 0;i<26;i++){
if(tem->next[i]!=NULL)
{
acnode *p;
if(tem == root){
tem->next[i]->fail = root;
}
else
{
p = tem->fail;
while(p!=NULL){
if(p->next[i]!=NULL){
tem->next[i]->fail = p->next[i];
break;
}
p=p->fail;
}
if(p==NULL)
tem->next[i]->fail = root;
}
q.push(tem->next[i]);
}
}
}
}
//匹配函數
void ac_automation(char *ch)
{
acnode *p = root;
int len = strlen(ch);
for(int i = 0; i < len; i++)
{
int x = ch[i] - 'a';
while(p->next[x]==NULL && p != root)//沒匹配到,那麼就找fail指針。
p = p->fail;
p = p->next[x];
if(!p)
p = root;
acnode *temp = p;
while(temp != root)
{
if(temp->sum >= 0)
/*
在這裏已經匹配成功了,執行想執行的操作即可,怎麼改看題目需求+
*/
{
cnt += temp->sum;
temp->sum = -1;
}
else break;
temp = temp->fail;
}
}
}
int main()
{
cnt = 0;
int n;
cin>>n;
char c[101];
root = newnode();
for(int i = 0 ;i < n;i++){
scanf("%s",c);
Insert(c);
}
getfail();
int m ;
cin>> m;
for(int i = 0;i<m;i++){
scanf("%s",c);
ac_automation(c);
}
cout<<cnt<<endl;
return 0;
}
ICPC慘敗,還是得努力啊!!!!