1. 程式人生 > >KMP——強大的next數組

KMP——強大的next數組

prope 找到 最大值 [] 最短 答案 下標 math 原理

\(KMP\) 的原理不在這裏仔細講了,主要說說最近刷題總結出的 \(next\) 數組的強大功能。
部分例題來自《信息學奧賽一本通》的配套練習。


基於定義——字符串相同前後綴

“基於定義”:我們求的 \(next\) 數組就是字符串到某一位時最長相同前後綴的長度。
註意 \(next\) 數組求的為“最長”的,那如果想知道一個字符串所有相同的前後綴長度咋辦?

舉個栗子:
技術分享圖片

假設一個 \(n\) 位的字符串(下標從 \(1\)\(n\)),\(next[n]=p\)
那麽該字符串的子串 \([1,p]\)\([n-p+1,n]\) 應是相同的
\(next[p]=q\) ,那麽子串 \([1,q]\)

\([p-q+1,p]\) 是相同的
綜上,子串 \([1,q]\)\([n-q+1,n]\) 是相同的,即 \(next[next[n]]\) 也是該字符串相同前後綴長度

就這樣 \(next\) 一遍遍向前找,直到某一位的 \(next\)\(0\), 拓展出一棵 \(next\) 樹(也叫 \(fail\) 樹)。

例題 \(bzoj3620\)

\(PROBLEM:\)
求一個長度為 \(n\) 的字符串所有形似 \(A+B+A\) , 且 \(len(A) \geq k,len(B) \geq 1\) 的子串數目。
\(n \leq 15000\)

\(SOLUTION:\)


一個奇妙的事情是這個題 \(O(n^2)\) 能過。
於是枚舉每一位為起點,\(KMP\) 的過程中,\(next\) 值相當於這一段子串 \(len(A)\) 的最大值
如果它小於 \(k\) ,顯然不行。
而若 \(它 \times 2+1 > 子串長度\) 也不行。
所以需要找到合適的 \(len(A)\) 滿足 \(len(A) \geq k\)\(len(A) \times 2 +1 \leq 子串長度\)
這就用到 \(next\) 樹的思想了!
還有一個小優化,用一個數組記錄某一段 \(\geq k\) 的最短的相同前後綴長度,把它作為 \(len(A)\) 判斷比較快。

代碼:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
 
using namespace std;
 
const int N = 15005;
 
char s[N];
int nxt[N],KK,ok[N],ans;
 
void KMP(char p[]){
    int len=strlen(p+1),k=0;
    nxt[1]=0; ok[0]=ok[1]=-1;
    for(int i=2;i<=len;i++){
        while(k && p[i]!=p[k+1]) k=nxt[k];
        if(p[i]==p[k+1]) k++;
        nxt[i]=k;
         
        if(k<KK) { ok[i]=-1; continue; }
        if(ok[k]==-1) ok[i]=k;
        else ok[i]=ok[k];
        if(ok[i]*2+1<=i) ans++;
    }
}
 
int main()
{
    int len;
    scanf("%s",s+1);
    scanf("%d",&KK);
    len=strlen(s+1);
     
    for(int i=1;i<=len;i++) {
        if(KK*2+1>len-i+1) break;
        KMP(s+i-1);
    }
    printf("%d\n",ans);
     
    return 0;
}

拓展功能——字符串循環節

“拓展”:這裏的主角為 \(n-next[n]\)

還是舉個栗子:
技術分享圖片

上圖中 \(n-next[n]=3\) ,那 \(3\) 是什麽呢?
看那些棕圈圈,\(3\) 其實可以叫做字符串的 “類”循環節,因為字符串並不是由這個循環節完完整整組成的。

而若一個字符串有真正的循環節要滿足什麽條件呢?
答案是 \(n-next[n]\) 整除 \(n\)
同樣舉個栗子就明了了:
技術分享圖片

對於所有字符串, \(n-next[n]\) 只是它最短的循環節(類循環節),其他循環節(類循環節)的長度通過 \(next\) 一遍遍向前找求出。
還是 \(next\) 樹的思想,結合栗子即可證明,這裏就不贅述了。

!!!
註意:有真正循環節的字符串,所有循環節長度都為最短循環節長度的倍數。而類循環節並不滿足這一性質!

例題1 \(bzoj1511\)

\(PROBLEM:\)
一個串是有限個小寫字符的序列,特別的,一個空序列也可以是一個串. 一個串 \(P\) 是串 \(A\) 的前綴, 當且僅當存在串 \(B\) , 使得 \(A = PB\). 如果 \(P \neq A\) 並且 \(P\) 不是一個空串,那麽我們說 \(P\)\(A\) 的一個 \(proper\) 前綴. 定義 \(Q\)\(A\) 的周期, 當且僅當 \(Q\)\(A\) 的一個 \(proper\) 前綴並且 \(A\)\(QQ\) 的前綴(不一定要是 \(proper\) 前綴). 比如串 \(abab\)\(ababab\) 都是串 \(abababa\) 的周期. 串 \(A\) 的最大周期就是它最長的一個周期或者是一個空串(當 \(A\) 沒有周期的時候), 比如說, \(ababab\) 的最大周期是 \(abab\). 串 \(abc\) 的最大周期是空串. 給出一個串,求出它所有前綴的最大周期長度之和.
\(串長度 \leq 10^6\)

\(SOLUTION:\)
其實題中說的最大周期就是 \(\neq A\) 的最長“類循環節”
\(next\) 樹的思想,用一個數組記錄每個“點”在該“樹”上最小的非零祖先,否則會超時

#include<cstdio>
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1000005;
typedef long long ll;

int n;
int nxt[N],snxt[N];
char s[N];

int main()
{
    scanf("%d",&n);
    scanf("%s",s+1);
    
    ll ans=0;
    int k=0;
    nxt[1]=0; snxt[1]=1;
    for(int i=2;i<=n;i++){
        while(k && s[i]!=s[k+1]) k=nxt[k];
        if(s[i]==s[k+1]) k++;
        nxt[i]=k; 
        snxt[i]=(k?snxt[k]:i);
        ans+=i-snxt[i];
    }
    printf("%lld\n",ans);
    
    return 0;
}

例題2 \(bzoj4974\)

\(PROBLEM:\)
一個串 \(T\)\(S\) 的循環節,當且僅當存在正整數 \(k\),使得 \(S\)\(T^k\) (即 \(T\) 重復 \(k\) 次)的前綴,比如 \(abcd\)\(abcdabcdab\) 的循環節。給定一個長度為 \(n\) 的僅由小寫字符構成的字符串 \(S\), 請對於每個 \(k(1 \leq k \leq n)\),求出 \(S\) 長度為 \(k\) 的前綴的最短循環節的長度 \(per_i\) 。小 \(Q\) 告訴你 \(n\) 以及 \(per_1,per_2,...,per_n\),請找到一個長度為 \(n\) 的小寫字符串 \(S\),使得 \(S\) 能對應上 \(per\)
\(n \leq 10^5\)

\(SOLUTION:\)
可以發現,\(per_i\) 值其實就是最短“類循環節”長度,也就是 \(n-next[i]\)
於是我們可以求出所有 \(next\) 值,然後進行逆向 \(KMP\) ,得出原字符串。

代碼:

#include<cstdio>
#include<iostream>
#include<algorithm>
 
using namespace std;
 
const int N = 100005;
 
int n;
int nxt[N],vis[26];
char s[N];
 
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&nxt[i]),nxt[i]=i-nxt[i];
     
    s[1]=‘a‘;
    for(int i=2;i<=n;i++){
        if(nxt[i]!=0) { s[i]=s[nxt[i]]; continue; }
        for(int j=0;j<26;j++) vis[j]=0;
        int k=nxt[i-1];
        while(k!=0) vis[s[k+1]-‘a‘]=1,k=nxt[k];
        vis[s[k+1]-‘a‘]=1;
        for(int j=0;j<26;j++)
            if(!vis[j]) { s[i]=‘a‘+j; break; }
    }
    printf("%s",s+1);
     
    return 0;
}

KMP——強大的next數組