終於開始學習新的東西了,總結一下字符串的一些知識。c++
即將一個字符串轉化成一個整數,並保證字符串不一樣,獲得的哈希值不一樣,這樣就能夠用來判斷一個該字串是否重複出現過。算法
因此說\(Hash\)就是用來求字符串是否相同或者包含的。(包含關係就能夠枚舉區間,可是一般用\(KMP\),不會真的有人用看臉的\(Hash\)作字符串匹配吧,不會吧不會吧)。數組
實現方式也是比較簡單的,其實就是把一個字符串轉化爲數字進行比較,到這裏可能有人就會說,直接比較長度和\(ASCII\)碼不就好了,也是轉化成數字啊(放屁)。這樣顯然是不行的,就比如說"ab"和「ba「,這兩個顯然不同,可是若是按上邊說的進行比較就是同樣的,這樣就錯了,因此咱們要換一種方式:改變一下進制。函數
若是是一個純字符串的話,那麼咱們應該把進制調到大於\(131\),由於若是小於,就不能給每一種的字符一個值,那麼正確性也就沒法保證了。因此取一個\(233\),合情合理,還很sao(逃。由於這個值至少能保證不會炸。咱們求出來每一個字符串對應的數字,而後進行比較就行了。學習
對於哈希而言,咱們認爲對一個數取模後同樣,那麼就是同樣的,因此能夠偷點懶,也就是天然溢出,使用\(unsigned\ long\ long\),至關於自動對\(2^{64}\)取模,而後進行比較便可,固然,能夠本身背一個\(10^{18}\)的質數進行取模(畢竟也是能卡的,也不知道哪一個毒瘤會卡),各有優缺點。spa
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\)了。因此要用到以前說的東東:前綴和。code
咱們記錄每一位的前綴和,而記算的時候須要乘以當前位的進制,這樣就會避免上邊說到的那種迷惑錯誤。記錄的時候就照常按照前綴和記錄,只須要最後改一下判斷就行。blog
定義\(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); }
學長說很不經常使用,因此理解一個思想便可。
\(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'\)迴文區域和左邊界的關係進行查找。
#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; }
正常咱們查找字符串是否爲子串的時候,每每都是暴力枚舉,效率爲\(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\)。
#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; }