字符串border原理小結&KMP優化


參考資料:
https://www.cnblogs.com/chasedeath/p/13396877.html

https://www.cnblogs.com/y-dove/p/14514097.html

字符串 \(s\)border:若 \(s\) 的一個子串既是它的前綴又是它的后綴,則這個子串是它的border(一般不包含本身)

字符串 \(s\)period:循環節。用前 \(T\) 個字符向后不斷復制,能得到 \(s\),最后一次可以只復制一部分


引理1:如果有一個border \(k\) 長度大於 \(s\) 的一半,可以得出得 \(s\) 有周期 \(|s|-|k|\)

同時,中間重疊部分是一個border,綠色部分構成了一組等差數列border:

0

1

\(T\) 個字符是周期的話,后 \(T\) 個顯然也是周期:

2

當然,這些都不重要


引理2:如果 \(p,q\) 都為周期,則 \(gcd(p, q)\) 也為周期

感性理解非常顯然,證明同樣不難,不做贅述了


引理3:字符串 \(s\) 所有不小於 \(|s|\) 一半的border構成一個等差數列

顯然有\(|s|-|u|\) 為最小周期,能組成一個公差為 \(|s|-|k|\) 的等差數列;

證明不存在這個等差數列之外的border:

假設 \(u\) 是字符串的最大border,設另一個不小於 \(|s|\) 一半的border為 \(v\)

由引理1,2可得,\(gcd(|s|-|u|,|s|-|v|)\) 是一個周期,而 \(|s|-|u|\) 已經是最小周期了,所以 \(|s|-|v|\) 為最小周期的整數倍,得證。


引理4:可以把字符串分成 \(log|s|\) 段,每一段的border都是一個等差數列

引理3的擴展,建議先看下面的應用,再回來就很好理解了。

先把 \(s\) 分成兩半,由引理3,右邊的border是等差數列(這里的border指:以右邊點為終點的前綴作為border)

左邊的border呢?再拆成一半,新的右半邊又構成了等差數列。。。

所以一個字符串的所有border可以被我們分成log數量級個等差數列。


應用:為什么要考慮border中的等差數列?

4

如圖所示,\(s\) 的所有成等差數列的border,其下一位一定相同

在KMP匹配中,我們可以利用這個性質快速跳過一串border

具體而言,在一次跳border時,如果發現border長度不小於原串的一半,則接下來的border構成等差數列,直到一半以下(引理3)

可以直接跳到 \((x-(x/2/d)*d)\) 處,即比一半大的第一個位置(整除)

(網上博客直接跳到了 \(x\%d+d\) 處,經過幾道題檢驗也是對的,但不是很能理解)

一次至少跳一半,保證 \(log\) 次以內可以跳完

\(\quad\)

例題:P5829 【模板】失配樹

標解是建樹后LCA,我們這個穩定跳log次border的“暴力匹配”可以更優雅地過這個題

int n, m;
string s;
int nt[maxn];

void get_next(){
    nt[1] = 0;
    for(int i=2;i<=n;i++){
        int p = nt[i-1];
        while(p && s[p+1] != s[i]) p = nt[p];
        if(s[p+1] != s[i]) nt[i] = 0;
        else nt[i] = p + 1;
    }
}

void solve() {
    cin >> s; n = s.size();
    s = '0' + s;
    get_next();
    cin >> m;
    while(m--){
        int x, y; cin >> x >> y;
        x = nt[x], y = nt[y];
        while(x != y){
            if(x < y) swap(x, y);
            if(nt[x] > x/2){
                int d = x - nt[x];
                if(y % d == x % d) x = y;
                else x = x - (x/2/d) * d;//大優化
            }else x = nt[x];
        }
        cout << x << '\n';
    }
}

\(\quad\)

例題:2021ICPC沈陽站M

當前位置的答案即為上一位置的答案的某個border加上新字符;找到最小的 加上新字符后大於原答案的border即可。(有一定難度,具體見題解)

int n, m;
string s;
int nt[maxn];

int head = 0;
void expend_nt(int frm, char c){
    if(frm == 0){
        nt[frm+1] = 0;
        return;
    }
    int p = nt[frm];
    while(p && s[head+p+1] != c) p = nt[p];
    if(s[head+p+1] != c) nt[frm+1] = 0;
    else nt[frm+1] = p + 1;
}

void solve() {
    cin >> s; n = s.size();
    s = '0' + s;
    nt[1] = 0;
    cout << "1 1\n";
    int lst = 1;
    for(int i=2;i<=n;i++){
        int x = lst, len = lst + 1;
        while(x > 0){
            if(s[i] > s[i-lst+x]) len = x + 1;
            if(nt[x] > x/2){
                int d = x - nt[x];
                x = x % d + d;
            }else x = nt[x];
        }
        if(s[i] > s[i-lst]) len = 1;
        head = i - len;
        expend_nt(len-1, s[i]);
        lst = len;
        cout << i-len+1 << ' ' << i << '\n';
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM