1. 程式人生 > 實用技巧 >擴充套件 KMP(Z 函式)

擴充套件 KMP(Z 函式)

能處理的問題形式

線性求:\(\operatorname {Lcp}([i:],[:n]),i\in [1,n]\)


演算法流程

回想一下 KMP 中我們記錄的是什麼?

\(nxt_i\) 表示 \([:i]\) 這個串中最長字首字尾匹配長度。

而現在丟給我們的問題是:求 \([i:]\)\([:n]\) 的最長字首匹配,我們用 \(z_i\) 來表示這個值。

對於當前知道的右端點最靠右的匹配串 \([l:r]\)(與 \([1:r-l+1]\) 匹配),若有 \(i\le r\),那麼 \([i:r]\)\([i-l+1:r-l+1]\) 匹配。

我們如何利用當前已知的東西求出匹配長度?

\([i-l+1:r-l+1]\)\([:n]\) 嘗試匹配,可以看做是 \([i-l+1:]\)\([:n]\) 嘗試匹配後,其大小與 \(r-i+1\) 取一個 \(\min\) 值。

而這個 \([i-l+1:]\)\([:n]\) 的匹配,則可以看做是相同型別的問題,就是 \(z_{i-l+1}\)

那麼這個轉移可以看做是沒有問題了,你已經完全理解了為什麼這麼轉移是對的,剩下部分可能還有一些往後擴充套件的,直接暴力掃過去。


複雜度分析(也許算不上嚴謹的分析)

接下來就是考慮複雜度為什麼是對的?

我們維護的 \(r\),實際上是 \([i:]\) 的最大可轉移字首的右端點,我們每次暴力擴充套件的時候,實際上就會將 \(r\)

更新。

那麼這裡的 \(l,r\) 就是雙指標,這樣的存在。

雖然說得比較模糊,但是確實就是 \(\operatorname O(n)\) 的。


模板程式碼

#include <stdio.h>
#include <string>
#include <string.h>
#include <iostream>
#define LL long long
using namespace std;
const int N=2e7+3;

inline int min(int x,int y){return x<y?x:y;}
inline LL min(LL x,LL y){return x<y?x:y;}

char a[N];
char b[N];
int n,m;

LL z[N];
LL ans;
inline void init()
{
    LL l,r;
    l=r=0;
    z[1]=m;
    ans=m+1;
    for(LL i=2;i<=m;i++)
    {
        if(i<=r)z[i]=min(z[i-l+1],r-i+1);
        for(;z[i]+i<=m&&b[z[i]+1]==b[z[i]+i];z[i]++);
        if(i+z[i]-1>r)l=i,r=i+z[i]-1;
        ans^=(z[i]+1)*i;
    }
    printf("%lld\n",ans);
    return;
}
LL p[N];
inline void work()
{
    LL l,r;
    l=r=ans=0;
    for(LL i=1;i<=n;i++)
    {
        if(i<=r)p[i]=min(z[i-l+1],r-i+1);
        for(;p[i]+1<=m&&p[i]+i<=n&&b[p[i]+1]==a[p[i]+i];p[i]++);
        if(i+p[i]-1>r)l=i,r=i+p[i]-1;
        ans^=(p[i]+1)*i;
    }
    printf("%lld\n",ans);
    return;
}
int main()
{
    // freopen("P5410_2.in","r",stdin);
    int i,j;
    cin>>(a+1)>>(b+1);
    n=strlen(a+1);m=strlen(b+1);
    a[n+1]=5;
    b[m+1]=10;
    init();
    work();
    return 0;
}