字串學習筆記

liuchanglc發表於2020-07-23

一、字串雜湊

定義

字串雜湊實質上就是把每個不同的字串轉成不同的整數
這樣相對於儲存整個字串來說佔用的空間更少,而且也便於比較

實現

我們可以把每一個字元想象成一個數字,然後確立一個進位制\(bas\)
比如一個字串\(abc\)
我們可以把它表示為\((c-a+1)\times bas^{0} + (b-a+1)\times bas^{1} +(a-a+1)\times bas^{2}\)
這裡有幾個需要注意的地方
首先進位制的選擇要大於字元的種類數,否則會有很大的概率出現衝突
還有就是我們在把字元轉成整形的時候,可以直接使用它的\(ASCII\)碼值,也可以用它減去一個字元
但是在使用第二種方法的時候,減去一個字元後要加上一個\(1\),否則會出現錯誤
比如字串\(aaa\)\(aa\),如果我們將每一個字元減去\(a\)後不把它加上\(1\)的話
最後兩個字串的雜湊值都會變成\(0\),也就是說會把這兩個字串判成相等,會出現錯誤的結果
由於字串的長度可能很大,因此如果我們一直把它的雜湊值累加的話,很有可能會溢位
因此,我們要對某個字串的雜湊值取模,方法有兩種
一種是選取一個較大的質數
比如\(19260817\)\(19660813\)\(1222827239\)\(212370440130137957\)
另一種是使用\(unsigned long long\)使其自然溢位
其實後一種方法就相當於對\(2^{64}-1\)取模
還有一種操作是取出字串中某一段字元\([l,r]\)\(hash\)
這時我們要用到一個公式\(ha[r]-ha[l-l]*pw[r-l+1]\)
其中\(ha[i]\)為該字串前\(i\)位的\(hash\)值,\(pw[i]\)為進位制\(bas\)\(i\)次方

程式碼實現

我們拿洛谷P3370來舉例子
這裡我用的是自然溢位

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ll;
const int maxn=1e5+5;
ll f[maxn];
ll bas=233,cnt=0;
ll get_hash(char s[]){
    ll ans=0,len=strlen(s);
    for(ll i=0;i<len;i++){
        ans=ans*bas+s[i];
    }
    return ans;
}
char s[maxn];
int main(){
    int n;
    scanf("%d",&n);
    while(n--){
        scanf("%s",s);
        f[++cnt]=get_hash(s);
    }
    sort(f+1,f+1+cnt);
    int now=1;
    for(ll i=2;i<=cnt;i++){
        if(f[i]!=f[i-1]) now++;
    }
    printf("%d\n",now);
}

二、KMP字串匹配

定義

\(KMP\)演算法是一種改進的字串匹配演算法,由\(D.E.Knuth,J.H.Morris\)\(V.R.Pratt\)提出的,因此人們稱它為克努特—莫里斯—普拉特操作(簡稱\(KMP\)演算法)。\(KMP\)演算法的核心是利用匹配失敗後的資訊,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是通過一個\(next()\)函式實現,函式本身包含了模式串的區域性匹配資訊。\(KMP\)演算法的時間複雜度\(O(m+n)\)
通俗的來說就是在需要匹配的那個串上給每個位置一個失配指標\(fail[j]\),表示在當前位置j失配的時候需要返回到\(fail[j]\)位置繼續匹配,而這就是KMP演算法優秀複雜度的核心。

實現

我們設\(fail[i]\)為第\(1\)-第\(i\)位中字首與字尾相同的部分最長是多長。
這樣,即可以理解為,若第\(i\)位失配了,則至少要往前跳多少步,才可能重新匹配得上。
我們拿實際的圖來演示一下

目前,我們匹配到了\(i-1\)的位置,\(fail[i-1]=j\)
即圖中劃黃色線的部分完全相同
我們拿當前的\(fail[i-1]\)去繼續匹配
如果\(s[i]=s[j+1]\)那麼\(fail[i]\)更新為\(j+1\)即可
如果\(s[i] \neq s[j+1]\)那麼如果按照暴力的思路,我們會把\(j--\)繼續匹配
但是實際上,我們可以直接從\(fial[j]\)的位置開始匹配
因為圖中兩個藍色的部分完全相等,而根據黃色的部分完全相等
我們又可以知道從\(i-1\)開始也有一個藍色的部分和它相等
這時我們只需要判斷\(s[i]\)\(s[fail[j]+1]\)的關係就可以了
如果不存在,則繼續跳\(fail\)
易證當前一定是次優解

程式碼實現

我們拿洛谷P3375來舉例子

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
char s[maxn],s1[maxn];
int f[maxn];
int main(){
    scanf("%s%s",s+1,s1+1);
    int l=strlen(s+1);
    int l1=strlen(s1+1);
    for(int i=2,j=0;i<=l1;i++){
        while(j && s1[i]!=s1[j+1]) j=f[j];
        if(s1[i]==s1[j+1]) f[i]=++j;
    }
    for(int i=1,j=0;i<=l;i++){
        while(j && s[i]!=s1[j+1]) j=f[j];
        if (s[i]==s1[j+1]) j++;
        if(j==l1){
            printf("%d\n",i-l1+1);
            j=f[j];
        }
    }
    for(int i=1;i<=l1;i++){
        printf("%d ",f[i]);
    }
    printf("\n");
    return 0;
}

三、manacher演算法

定義

馬拉車\((Manacher)\)演算法是在\(O(n)\)時間內解決尋找源字串的最長迴文子串\(S\)的問題的演算法。

實現

首先我們要知道,迴文串分為奇迴文串和偶迴文串
\(aaaa\)這樣的就是偶迴文串,而\(aba\)則是奇迴文串
不難發現,奇迴文串都有一個迴文中心,因此在查詢時可以由中心向兩邊擴充套件
但是偶迴文串則沒有這一個性質,因此查詢起來不如奇迴文串方便
為了使查詢更方便,我們可以讓所有的偶迴文串都變成奇迴文串
操作實現也很簡單,就是將原字串的首部和尾部以及每兩個字元之間插入一個特殊字元,這個字元是什麼不重要,不會影響最終的結果
同時還要在隊首之前再插入另一種特殊字元,防止運算時越界
比如\(abaca\)擴充套件後變為\(#*a*b*a*c*a*\)
在進行馬拉車演算法時,我們要維護一個已經確定的右側最靠右的迴文串的右邊界\(r\)和迴文中心\(mids\)
同時定義一個陣列\(f[i]\)為以\(i\)為中心的最大回文半徑
當我們遍歷到\(i\)時,如果\(i\)在右邊界之內
那麼根據對稱性,有\(f[i]=f[s*mids-i]\)
同時,\(i\)所擴充套件的範圍必須在\(r\)之內,因此結果還要與\(r-i+1\)\(min\)
擴充套件完已知的區域,我們再向兩邊擴充套件未知的區域
最後我們更新\(mids\)\(r\)即可
最後的答案就是最大回文半徑減去一,手模一下即可

程式碼實現

我們拿洛谷P3805來舉例子

#include<bits/stdc++.h>
using namespace std;
const int maxn=22e6+5;
char s1[maxn],s[maxn];
int f[maxn],ans,n,cnt;
int main(){
    scanf("%s",s1+1);
    n=strlen(s1+1);
    cnt=2*n+1;
    for(int i=1;i<=cnt;i++){
        if(i&1) s[i]='&';
        else s[i]=s1[i/2];
    }
    s[0]='%';
    for(int i=1,mids=0,r=0;i<=cnt;i++){
        if(i<=r) f[i]=min(f[2*mids-i],r-i+1);
        while(s[i+f[i]]==s[i-f[i]]) f[i]++;
        if(i+f[i]>r) r=i+f[i]-1,mids=i;
        if(f[i]>ans) ans=f[i];
    }
    printf("%d\n",ans-1);
    return 0;
}

相關文章