字符串學習筆記一


一、字符串哈希

定義

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

實現

我們可以把每一個字符想象成一個數字,然后確立一個進制\(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\)次方

二維哈希

如果我們要對一個矩陣進行哈希,並且要求能夠快速地取出其中某一個子矩陣的哈希值,就要用到二維哈希
思路大體上和一維的哈希一樣,但是橫向和縱向要設置兩個不同的 \(base\) 來防止沖突

對矩陣進行哈希

basa[0]=basb[0]=1;
for(rg int i=1;i<=n;i++) basa[i]=basa[i-1]*bas1;
for(rg int i=1;i<=m;i++) basb[i]=basb[i-1]*bas2;
for(rg int i=1;i<=n;i++){
	for(rg int j=1;j<=m;j++){
		has[i][j]=has[i][j-1]*bas1+a[i][j];
	}
}
for(rg int i=1;i<=n;i++){
	for(rg int j=1;j<=m;j++){
		has[i][j]+=has[i-1][j]*bas2;
	}
}

取出右上角為\((x,y)\),邊長為\(len\)的矩形的哈希值

ull gethash(rg int x,rg int y,rg int len,rg int op){
	return has[x][y][op]-has[x-len][y][op]*basb[len]-has[x][y-len][op]*basa[len]+has[x-len][y-len][op]*basa[len]*basb[len];
}

代碼實現

我們拿洛谷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;
}

下接字符串學習筆記二


免責聲明!

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



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