淺談單模式串字串匹配演算法(KMP)
字串演算法很有趣,尤其是KMP和AC自動機~~
大綱
1.問題定義
字串匹配是電腦科學中最古老、研究最廣泛的問題之一。一個字串是一個定義在有限字母表∑上的字元序列。例如,ATCTAGAGA是字母表∑ = {A,C,G,T}上的一個字串。字串匹配問題就是在一個大的字串T中搜索某個字串P的所有出現位置。其中,T稱為文字,P稱為模式,T和P都定義在同一個字母表∑上。
還是比較好理解的,這段就過了吧
2.實現
1.樸素
俗話說,暴力出奇跡。這種匹配問題給人的第一直覺就是用暴力,一個個比嘛,不匹配就換下一個唄
char T[maxn],P[ maxn]
void check()
{
int lenT=stren(T);
int lenP=strlen(P);
int i = 0; // 主串的位置
int j = 0; // 模式串的位置
while (i < t.length && j < p.length)
{
if (t[i] == p[j]) { i++;j++;}
else
{
i = i - j + 1;
j = 0;
}
}
if ( j == p.length) return i - j;
else return -1;
}
...
但是,這份程式碼與接下來要分析的KMP比起來,差了太多,因為它的時間複雜度太大
2.KMP
1.思想
假設現在有一主串T,一模式串P
當匹配到P的最後一個字元時發現失配了,那麼上面那段暴力程式碼就會將P整體後移1位然後再重複
但不難發現,P中其實有我們可以利用的東西
P的第1,2位和失配的最後一位字元的前兩位字元完全一樣
既然我們已經把1~7位都匹配了一遍況且第1,2位和6,7位相等,這也就意味著T的6,7位和P的1,2位是等價的,於是就可以有下面的操作
可以用幾個式子來概括當T[i]與P[j]失配時,可以直接將P向後移動k個位置而不用依次比較的原因(來自https://www.cnblogs.com/yjiyjige/p/3263858.html)
這就是它的思想,不做無用功~~
流程(以luogu3375為例)
nxt陣列
上文已經提到過,當失配時,我們不需要像暴力程式碼一樣一個個跳,而是可以通過“智慧的手段”來減少工作量。而這個“智慧的手段”是什麼?其實就是大名鼎鼎的next陣列(為了避免關鍵字重名,一般使用nxt)
什麼是nxt陣列?
nxt[i]就是指模式串中以第i-1項結尾的字串的最長公共真前後綴的長度,其性質是
nxt陣列的作用
細心一點就可以看出來,上式中的nxt[i]就是上文裡講到的k(移動的位數)
因此當失配時,直接讓失配時的指標跳nxt陣列就好了
假設我們此時已經有了nxt陣列,那麼KMP的大致框架就出來了
#define clean(arry,num); memset(arry,num,sizeof(arry));
#define loop(i,start,end) for(int i=start;i<=end;i++)
#define anti_loop(i,start,end) for(int i=start;i<=end;i--)
#define printarry(arry,size) for(int i=1;i<=size;i++)printf("%d ",arry[i]);
void KMP()
{
int len1=strlen(s1);
int len2=strlen(s2);
int i=0;int j=0;
while(i<len1)
{
if(j==len2-1&&s1[i]==s2[j])printf("%d\n",i-len2+2);
if(j==-1||s1[i]==s2[j]){i++;j++;}
else j=nxt[j];
}
printarry(nxt,len2);
}
如何求得nxt陣列
現在我們離KMP就差一個nxt陣列啦
先給出求nxt的程式碼,看看能看懂不
void getnxt()
{
nxt[0]=-1;//初值
int len=strlen(s2);//s2是模式串,就是P
int j=-1;
int i=0;
while(i<len)
{
if(j==-1||s2[i]==s2[j]){nxt[++i]=++j;}
else j=nxt[j];
}
return;
}
在這份程式碼中,我們用了i,j兩個指標來維護nxt陣列,外層的while是nxt的位數(從第0位列舉到最後一位),i就是外層的指標
然後的if…else是精華,它以j為指標,是為了維護i的nxt值,這其中包含了3種情況:
1.當j實際指向第1個字元時
2.當i,j所指的字元相同時
3.當i,j指向的字元不同時
第一種情況和第二種情況都比較好解決,直接就把i,j向後移一位,然後用j的值來更新nxt[i](只要i,j所指的字元相同,那麼其前面的字元也都相等),其在程式碼中就體現在
if(j==-1||s2[i]==s2[j]){nxt[++i]=++j;}
第三種情況實際上是一個遞迴,只是這個遞迴沒有用函式呼叫的方式
當第三種情況出現時,可以肯定j以前的nxt都求出來了,而此時的s2[i]!=s2[j],也就意味著s2的nxt[j]項可能與s2[i]相同,於是將j賦值nxt[j],繼續迴圈,此時,如果j仍然不滿足要求,那就又繼續賦值,直到找到符合要求的或是達到邊界(j=-1)
這期間,j的值是從於i相差1一直減小,最小到-1的
這體現在
else j=nxt[j];
這就是KMP的大致框架,下面附上程式碼
#include<bits/stdc++.h>
using namespace std;
#define clean(arry,num); memset(arry,num,sizeof(arry));
#define loop(i,start,end) for(int i=start;i<=end;i++)
#define anti_loop(i,start,end) for(int i=start;i<=end;i--)
#define printarry(arry,size) for(int i=1;i<=size;i++)printf("%d ",arry[i]);
const int maxn=1000000+10;
char s2[maxn],s1[maxn];
int nxt[maxn];
void calcnxt()
{
int len=strlen(s2);
int j=-1;
int i=0;
while(i<len)
{
if(/*j<=1*/j==-1||s2[i]==s2[j]){nxt[++i]=++j;}
else j=nxt[j];
}
return;
}
void KMP()
{
int len1=strlen(s1);
int len2=strlen(s2);
int i=0;int j=0;
while(i<len1)
{
if(j==len2-1&&s1[i]==s2[j])
{
printf("%d\n",i-len2+2);//
}
if(j==-1||s1[i]==s2[j]){i++;j++;}
else j=nxt[j];
}
printarry(nxt,len2);
}
int main()
{
freopen("datain.txt","r",stdin);
scanf("%s%s",s1,s2);
clean(nxt,0);
nxt[0]=-1;
calcnxt();
KMP();
return 0;
}
/********************************************************************
ID:Andrew_82
LANG:C++
PROG:KMP
********************************************************************/
感覺自己對這個演算法的理解又加深了
本文的部分思路來自以下幾個大佬的部落格
孤~影
北京小王子
若有不周之處,請多多指教