字符串學習總結(Hash & Manacher & KMP)


前言

終於開始學習新的東西了,總結一下字符串的一些知識。

NO.1 字符串哈希(Hash)

定義

即將一個字符串轉化成一個整數,並保證字符串不同,得到的哈希值不同,這樣就可以用來判斷一個該字串是否重復出現過。

所以說\(Hash\)就是用來求字符串是否相同或者包含的。(包含關系就可以枚舉區間,但是通常用\(KMP\)不會真的有人用看臉的\(Hash\)做字符串匹配吧,不會吧不會吧)。

實現

實現方式也是比較簡單的,其實就是把一個字符串轉化為數字進行比較,到這里可能有人就會說,直接比較長度和\(ASCII\)碼不就行了,也是轉化成數字啊(放屁)。這樣顯然是不行的,就好比說"ab"和“ba“,這兩個顯然不一樣,但是如果按上邊說的進行比較就是一樣的,這樣就錯了,所以我們要換一種方式:改變一下進制。

如果是一個純字符串的話,那么我們應該把進制調到大於\(131\),因為如果小於,就不能給每一種的字符一個值,那么正確性也就無法保證了。所以取一個\(233\),合情合理,還很sao(逃。因為這個值至少能保證不會炸。我們求出來每個字符串對應的數字,然后進行比較就好了。

對於哈希而言,我們認為對一個數取模后一樣,那么就是一樣的,所以可以偷點懶,也就是自然溢出,使用\(unsigned\ long\ long\),相當於自動對\(2^{64}\)取模,然后進行比較即可,當然,可以自己背一個\(10^{18}\)的質數進行取模(畢竟也是能卡的,也不知道哪個毒瘤會卡),各有優缺點。

代碼

ull Hash(char s[]){//ull自然溢出
	ull res = 0;
	int len = strlen(s);
	for(int i=0;i<len;++i){//計算每一位,用自己定義的進制base乘(也就是233 qwq)
		res = (res*base + (ull)s[i])%mod;//這里我是取了個玄學mod
	}
	return res;
}

以上就是整個字符串之間的對比。下邊說一說字符串里某個區間的對比

區間對比

意思就是直接給出你幾個字符串,對比每個字符串里給定的區間\([l,r]\),這樣的話如果直接一個個的掃,肯定會慢好多,如果直接求整個串然后相減,那么肯定是錯誤的,因為每一位都是要乘以一個進制的,如果直接計算,那么肯定就會亂掉,也就\(WA\)了。所以要用到之前說的東東:前綴和。

我們記錄每一位的前綴和,而記算的時候需要乘以當前位的進制,這樣就會避免上邊說到的那種迷惑錯誤。記錄的時候就照常按照前綴和記錄,只需要最后改一下判斷就行。

定義\(pw[len]\)為長度為\(len\)時的需要乘以的進制,前綴和就用\(sum\)來表示,求前綴和就是這樣:

int main(){
	cin>>s;
	int len = strlen(s);
	sum[0] = (ull)s[0];
	for(int i=1;i<len;++i){
		sum[i] = sum[i-1]*base+(ull)a[i];//乘以進制不能忘
	}
}

下邊是判斷是否合法:

	while(n--){
		int l,r,s,t,len;
		cin>>l>>r>>s>>t;
		len = r-l+1;//計算第幾位來乘以進制,pw數組提前可以快速冪處理好
		if(sum[r] - sum[l-1]*pw[len] == sum[t]-sum[s-1]*pw[len])printf("YES\n");//如果這樣計算出來值相等就合法
		else printf("NO\n");
	}

模板例題

字符串哈希

例題代碼

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
const ull mod = 1926081719260817;
const int maxn = 1e4+10;
ull base = 233;
int a[maxn];
char s[maxn];
ull Hash(char s[]){
	ull res = 0;
	int len = strlen(s);
	for(int i=0;i<len;++i){
		res = (res*base + (ull)s[i])%mod;
	}
	return res;
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		cin>>s;
		a[i] = Hash(s);
	}
	int ans = 1;
	sort(a+1,a+n+1);
	for(int i=1;i<n;++i){
		if(a[i] != a[i+1])ans++;
	}
	printf("%d\n",ans);
}

NO.2 Manacher算法

學長說很不常用,所以理解一個思想即可。

定義

\(1975\)年,\(Manacher\)發明了\(Manacher\)算法(中文名:馬拉車算法),是一個可以在\(O(n)\)的復雜度中返回字符串\(s\)中最長回文子串長度的算法,十分巧妙。
例如這個字符串:“abaca”,它可以處理每一位的回文字串,以\(O(n)\)的效率處理最大值(當然還是有擴展的,只不過它不太常用,就只是分析一下算法過程)

實現

因為回文串分為奇回文串和偶回文串,處理起來比較麻煩,所以我們要用到一個小(sao)技(cao)巧(zuo),在每兩個字符之間插入一個不會出現的字符,但是要求插入的字符一樣,這樣才能保證不影響回文串的長度。
舉個例子:“abbadcacda”這個字符串,我們需要插入新的字符,這里用’#',那么就有了如下對應關系:

其中定義\(p[i]\)為以\(i\)為半徑的回文半徑,也就是從中心向兩邊最長能拓展多少,而根據這個可以推出來以它為中心的真正的回文串的長度。也就是\(p[i]-1\),根據這個就可以得到最長的回文串的長度了。
但是復雜度為什么是\(O(n)\)呢,那么就涉及到了他的實現方法,我們定義一個回文中心\(C\)和這個回文的右側\(R\),也就是當前中心的最長回文的右端點,如果枚舉到的\(i\)大於\(R\),那么直接更新就行,但是如果在里邊,那么會分出來三種情況:
\(1\)、枚舉到的\(i\)關於\(C\)對稱到\(i'\),這時候\(i'\)的回文區域在\([L,R]\),那么\(i\)的回文半徑就是\(i'\):
證明:因為此時的\([L,R]\)就是一個回文區間,所以左右對稱過來是一樣的,所以得到\(i\)的回文半徑。

\(2\)、枚舉到\(i\),此時對稱點\(i'\)的回文區域超出了\(L\),那么\(i\)的回文區域就一定是從\(i\)\(R\)
證明:借用一張圖片便於解釋:

(圖好丑……)首先我們設\(L\)點關於\(i'\)對稱的點為\(L'\)\(R\)點關於\(i\)點對稱的點為\(R'\)\(L\)的前一個字符為\(x\)\(L’\)的后一個字符為\(y\)\(k\)\(z\)同理,此時我們知道\(L - L'\)\(i'\)回文區域內的一段回文串,故可知\(R’ - R\)也是回文串,因為\(L - R\)是一個大回文串。所以我們得到了一系列關系,\(x = y,y = k,x != z\),所以 \(k != z\)。這樣就可以驗證出\(i\)點的回文半徑是\(i - R\)
\(3\)\(i'\) 的回文區域左邊界恰好和\(L\)重合,此時\(i\)的回文半徑最少是\(i\)\(R\),回文區域從\(R\)繼續向外部匹配。
證明:因為 \(i'\) 的回文左邊界和L重合,所以已知的\(i\)的回文半徑就和\(i'\)的一樣了,我們設\(i\)的回文區域右邊界的下一個字符是\(y\)\(i\)的回文區域左邊界的上一個字符是\(x\),現在我們只需要從\(x\)\(y\)的位置開始暴力匹配,看是否能把\(i\)的回文區域擴大即可。

小小總結一下,其實就是先進行暴力匹配,然后根據\(i'\)回文區域和左邊界的關系進行查找。

例題+代碼

Manacher板子

#include<bits/stdc++.h>
using namespace std;
const int maxn = 11e6;
char s[maxn];
int Manacher(char s[]){
	int len = strlen(s);
	if(len == 0)return 0;//長度為0就return
	int len1 = len * 2 + 1;
	char *ch = new char[len1];//動態數組
	int *par = new int[len1];
	int head = 0;
	for(int i=0;i<len1;++i){
		ch[i] = (i & 1) == 0 ? '#' : s[head++];//插入不一樣的字符
	}
	int C = -1;
	int R = -1;
	int Max = 0;
	par[0] = 1;
	for(int i=0;i<len1;++i){//枚舉三種情況
		par[i] = (i < R)? min(par[C*2-i],R-i) : 1;//取最小的回文半徑
		while(i + par[i] < len1 && i - par[i] > -1&& ch[i + par[i]] == ch[i - par[i]]){//暴力匹配
			par[i] ++ ;
		}
		if(i + par[i] > R){//如果超過右邊界就更新
		 	R = i + par[i];
			C = i;
		}
		Max = max(Max,par[i]);//更新最大半徑
	}
	delete[] ch;//清空動態數組
	delete[] par;
	return Max - 1;//因為這個是添了字符的最大回文半徑,所以回文串的最長是它-1
}
int main(){
	cin>>s;
	cout<<Manacher(s);
	return 0;
}

NO.3 KMP算法

正常我們查找字符串是否為子串的時候,往往都是暴力枚舉,效率為\(O(n^2)\),但是字符串長了或者多了,肯定就是不行的了,所以有了\(KMP\)算法。

定義

\(KMP\)算法是一種改進的字符串匹配算法,由\(D.E.Knuth,J.H.Morris\)\(V.R.Pratt\)同時發現,因此人們稱它為克努特——莫里斯——普拉特操作(簡稱\(KMP\)算法)。\(KMP\)算法的關鍵是利用匹配失敗后的信息,盡量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是實現一個\(next\)函數,函數本身包含了模式串的局部匹配信息。時間復雜度\(O(m+n)\)

通俗的來說就是在需要匹配的那個串上給每個位置一個失配指針\(fail[j]\),表示在當前位置\(j\)失配的時候需要返回到\(fail[j]\)位置繼續匹配,而這就是\(KMP\)算法優秀復雜度的核心。

實現

失配數組的匹配就是把需要查找的那個字符串進行一遍前綴和后綴之間的匹配。我們舉個例子"ababa"這里真前綴分別為"a","ab","aba","abab",真后綴為"a","ba","aba","baba",找到他們的最大相同位置,就是\(fail\)指針,

我們設\(kmp[i]\) 用於記錄當匹配到模式串的第 \(i\) 位之后失配,該跳轉到模式串的哪個位置,那么對於模式串的第一位和第二位而言,只能回跳到 \(1\),因為是 \(KMP\)是要將真前綴跳躍到與它相同的真后綴上去(通常也可以反着理解),所以當 \(i=0\) 或者 \(i=1\) 時,相同的真前綴只會是 \(str1(0)\)這一個字符,所以\(kmp[0]=kmp[1]=1\)

模板+代碼

KMP字符串匹配

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6+10;
char a[maxn],b[maxn];
int kmp[maxn];
int main(){
	cin>>a+1>>b+1;
	int lena = strlen(a+1);
	int lenb = strlen(b+1);
	int j = 0;
	for(int i=2;i<=lenb;++i){//自己跟自己匹配處理出kmp數組
		while(j && b[i] != b[j+1]){
			j = kmp[j];
		}
		if(b[i] == b[j+1])j++;
		kmp[i] = j;
	}
	j = 0;
	for(int i=1;i<=lena;++i){
		while(j && a[i] != b[j+1]){
			j = kmp[j];
		}
		if(a[i] == b[j+1])j++;
		if(j == lenb){//匹配完了就輸出位置
			printf("%d\n",i-lenb+1);
			j = kmp[j];//返回失配位置
		}
	}
	for(int i=1;i<=lenb;++i){
		printf("%d ",kmp[i]);
	}
	return 0;
}



免責聲明!

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



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