字符串匹配的三個算法(KMP+字典樹+AC自動機)

字符串匹配的意思是給一個字符串集合,和另一個字符串集合,看這兩個集合交集是多少。

若是都只有一個字符串,那麼就看其中一個是否包含另外一個;

若是父串集合(比較長的,被當做模板)的有多個,子串(拿去匹配的)只有一個,就是問這個子串是否存在於父串之中;

若是子串父串集合都有多個,那麼就是問交集了。


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慘敗,還是得努力啊!!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章