<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      若干字符串算法

      Hash

      將不知道什么東西映射到一個小范圍的數(shù)上。相比用map而言,手寫Hash往往會做到更高的效率。
      Hash的一大用處是儲存和查詢兩個復雜數(shù)據(jù)的存在情況。這對于判斷多個字符串相等往往有很大優(yōu)勢。
      在OI中有一種非常重要的Hash函數(shù),它的運轉方式如下:

      把字符串\(s\)看成一個\(P\)進制數(shù),這樣就可以給每個數(shù)賦上一個正的權值。具體來說,它返回的是一個無符號的長整數(shù)(省去了取模操作),大小:

      \[H(s) = \sum_{i=0}^{|s|-1} s_i P^{i} \]

      \(P = 131\)\(P = 13331\)時,沖突的概率極低。

      另外,上面這個公式還有遞推版本和區(qū)間版本:(\(c\)是單個字符)

      \[H(s + c) = H(s)\cdot P + c \]

      \[H(l,r) = H(r) - H(l-1) \cdot P^{r-l+1} \]

      這樣,假設只有\(q\)個字符串,我們就可以用\(O(q|s|)\)的時間內處理每個字符串前綴的哈希值,并僅用\(O(1)\)判斷兩個字符串或者它們的子串是否相等了。

      單詞背誦。我給出了一個長度為\(N(N \leq 1000)\)的單詞表,希望背下它們。但我打算通過閱讀文章來背誦它們。具體來說,我有一篇含單詞數(shù)為\(M(M \leq 10^6)\)的文章,而我會節(jié)選其中連續(xù)的一段,將其提取,并每天閱讀。我希望求出這篇文章中最多涵蓋了多少我單詞表里的單詞,并且在涵蓋極大單詞的情況下,我最短可以摘選多短的文章。

      這里體現(xiàn)了Hash表的經(jīng)典應用,即快速查詢兩個字符串是否相等。可以預處理出文章和單詞表內所有單詞的哈希值,并二分查找文章中單詞對應單詞表里的單詞編號。此時問題變成了“涵蓋所有目標元素的極大子段”,先求出最多的單詞數(shù)\(cnt\),然后用兩個指針線性掃描文章。每擴展一次右邊界時,貪心地縮小左邊界,然后更新答案。預處理\(O(N\log N + M|s|\log N)\),而掃描是線性的。如果這道題對哈希值取合適的模數(shù),可以省去排序的步驟,把預處理降到\(O(M|s|)\);但為了防止哈希沖突,這里我們還是采用最穩(wěn)妥的做法。

      inline ull Hash(char *str)
      {
      	int len = strlen(str);
      	ull ret = 0;
      	RP(i, 0, len - 1) ret = ret * P + (str[i] - 'a' + 1);
      	return ret;
      }
      inline int bs(ull CH){
      	int l = 1, r = N;
      	while(l < r){
      		int mid = (l + r) >> 1;
      		if(HS[rev[mid]] >= CH)
      			r = mid;
      		else
      			l = mid + 1;
      	}
      	if(HS[rev[l]] == CH)
      		return rev[l];
      	else
      		return 0;
      }
      inline void init(){
      	RP(i, 1, N) HS[i] = Hash(S[i]), buf[i] = mp(HS[i], i);
      	sort(buf + 1, buf + N + 1);
      	RP(i, 1, N) disc[buf[i].second] = i, rev[i] = buf[i].second;
      
      	RP(i, 1, M) HT[i] = Hash(T[i]), match[i] = bs(HT[i]);
      }
      int main(){
      	N = qr(1);
      	RP(i, 1, N) scanf("%s", S[i]);
      	M = qr(1);
      	RP(i, 1, M) scanf("%s", T[i]);
      	init();
      	int l = 1, r = 1;
      	while(r <= M && cnt < N){
      		if(match[r] > 0 && buc[match[r]] == 0) ++ cnt;
      		++ buc[match[r]];
      		++ r;
      	}
      	if(cnt == 0){
      		puts("0");puts("0");
      		return 0;
      	}
          r = 1; int tmp = 0; memset(buc, 0, sizeof(buc));
      	while(r <= M && tmp < cnt){
      		if(match[r] > 0 && buc[match[r]] == 0) ++ tmp;
      		++ buc[match[r]];
      		++ r;
      	}
      	-- r;
      	while(r <= M){
      		while(l < r){
      			if(buc[match[l]] == 1)
      				break;
      			if(buc[match[l]]) -- buc[match[l]];
      			++ l;
      		}
      		ans = min(ans, r - l + 1);
      		++ r; ++ buc[match[r]];
      	}
      	printf("%d\n%d", cnt, ans);
      	return 0;
      }
      

      附:ELFHash

      偏工程向,但是還是可以拿來一用。其基本思想是每次載入當前字符的ASCLL碼,然后讓編碼相互雜糅,互相產生影響。具體的原理超出了本文的討論范圍,故這里只粘貼代碼:

      inline ull Hash(char *str)
      {
          ull ret = 0, x = 0;
      	while(*str){
      		ret = (ret << 4) + *str;
      		if((x = ret & 0xf000000L) != 0){
      			ret ^= (x >> 24);
      			ret &= ~x;
      		}
      		++ str;
      	}
      	return (ret & 0x7fffffff);
      }
      

      順帶一提,最近使用這個ELFhash,結果出現(xiàn)了相當罕見的Hash沖突。只能說酌情使用吧。

      KMP

      KMP利用一個nxt數(shù)組維護了“如果當前不匹配,我應該從哪里開始匹配起”。它一定是針對模式串而言的。具體來說,它保存的是\(p(i)=\max\{j, S_{i - j + 1\cdots i} = S_{1\cdots j}\}\),即當前前綴子串中,最長的前綴和后綴相等的長度。可以結合下面這個圖理解一下:
      png

      假設模式串\(S\)\(1\cdots i\)都匹配,唯獨在\(i+1\)不匹配,我們就要重新匹配。但是,我們不需要從頭開始,而是從\(\text{nxt}(i)\)開始。因為\(1\cdots i\)均匹配,\(i-\text{nxt}(i)+1\cdots i\)顯然也一定匹配。由于最長性,我們從\(\text{nxt}(i)\)開始匹配一定更優(yōu)。

      問題就在于,如何求\(\text{nxt}(i)\)?樸素的做法會達到\(O(|S|^2)\)。可以考慮這樣的思路:
      假設我從\(i\)擴展到\(i+1\),如果恰好\(S_{\text{nxt}(i)+1} = S_{i+1}\),那顯然是最好的。
      可大部分情況下不能直接擴展。我們需要在\(\text{nxt}(i)\)之前找到一個位置\(j\),使得\(S_{j+1} = S_{i+1}\),并令\(\text{nxt}(i+1) = j+1\)。這個過程正是一個KMP匹配的過程,因此可以把\(S\)本身當成一個文本串,用自己匹配自己。

      不過很抱歉的是,目前我也不能太理解這個精妙的算法,因此只能先張貼代碼:

      const int MAXL = 1e6 + 2;
      char T[MAXL], S[MAXL]; int lT, lS;
      int nxt[MAXL];
      
      inline void init(){
      	nxt[1] = 0;
      	int r = 0;
      	RP(i, 2, lS){
      		while(r > 0 && S[r + 1] != S[i])
      			r = nxt[r];
      		if(S[r + 1] == S[i])
      			++ r;
      		nxt[i] = r;
      	}
      }
      inline void KMP(){
      	int r = 0;
      	RP(i, 1, lT){
      		while(r > 0 && S[r + 1] != T[i])
      			r = nxt[r];
      		if(S[r + 1] == T[i])
      			++ r;
      		if(r == lS)
      			printf("%d\n", i - lS + 1), r = nxt[r];
      	}
      }
      
      int main(){
      	scanf("%s", T + 1);
      	scanf("%s", S + 1);
      	lT = strlen(T + 1), lS = strlen(S + 1);
      	init();
      	KMP();
      	RP(i, 1, lS) printf("%d ", nxt[i]);
      	return 0;
      }
      

      只能說這個算法和樹狀數(shù)組一樣,寫起來和記起來都不算難,但如何理解卻有相當大的難度。

      manacher

      回文串的定義:若一個字符串的每一個字符\(S_i\)都滿足\(S_i = S_{|S|+1-i}\),則\(S\)是一個回文串。當然,一個串\(S\)本身可能不是回文串,但它一定存在至少\(|S|\)個回文子串。(單個字符也要考慮!)一個回文子串的直徑就是它的長度。現(xiàn)在請你求出一個字符串\(S\)的所有回文子串中最長的回文直徑。

      回文串的判定比較復雜,分為奇回文串(形如\(ABA\))和偶回文串(形如\(BB\))。為了方便判定,我們在相鄰的字符之間加入'#'號,從而將任意回文串的判定改為“奇回文串的判定”。這等價于插入\(S_{0.5},S_{1.5},\cdots\)等字符。

      上面我所提到的“回文直徑”可以直接無視,因為比它更重要的是回文半徑:對于一個長度為\(d\)的奇回文串,它的回文半徑\(r = \frac{1}{2}(d-1)\)。考慮對于每一個字符,用暴力拓展回文半徑的算法,可以做到\(O(N^2)\)。有沒有更快的呢?

      算法中一個非常重要的優(yōu)化手段就是利用已知信息,用空間換時間。我們考慮若干個回文半徑之間的關系,發(fā)現(xiàn)了一個重要的關系:當先前某個點的回文半徑足夠大,足以覆蓋當前帶擴展點時,我們可以直接繼承先前的答案。什么意思呢?請看下圖:

      png

      如果當前待求的回文中心\(i\)在一個足夠長的回文串內,且\(i\)形成的回文串足夠小,那么一定存在一個\(i'\)\(i\)關于\(mid\)對稱,且這兩個回文串的長度是完全相同的。因此,\(i'\)的答案可以直接傳給\(i\)
      當然,還有兩種特殊情況:

      • \(i\)可以擴展到青色的部分,而\(i'\)做不到。這種情況下直接在\(i'\)的回文半徑的基礎上,繼續(xù)向右擴展即可。
      • \(i'\)可以擴展到青色的部分,而\(i\)做不到。此時\(i\)最多只能繼承\(R-i\)大小的半徑(即擴展的邊界不能超過紫色邊界)
        情況很多,但是可以直接令當前回文半徑\(r(i) = \min(r(i'), R - i)\),然后再嘗試向右擴展\(r(i) = r(i) + \delta r\)就可以了。這個做法可以解決一切問題。

      每次維護當前回文串的最右邊界\(R\),就可以盡可能多地覆蓋到小回文串。由于回文串可以\(O(1)\)繼承在當前“大回文串”內的所有答案,這個算法可以做到\(O(N)\)

      順便注意一下,為了防止擴展越界,我們會在整個字符串\(S\)的最前面和最后面加上一個特殊字符表示邊界,比如$ * @等。當然,你也可以直接用if特判。
      最后一定要注意,上述操作都是對于加入#的字符串而言。原串的最長回文子串長度,應該等于當前串中的\(r\mid_{\max} - 1\)

      int main(){
      	scanf("%s", buf);//buf是一個臨時輸入數(shù)組
      	int len = strlen(buf);
      	
      	S[1] = '#';
      	RP(i, 0, len - 1){
      		S[(i + 1) << 1] = buf[i];
      		S[((i + 1) << 1) + 1] = '#';
      	}
      	len = (len << 1) + 1;
      	S[0] = '$', S[len + 1] = '@';//起始符和終止符,防止越界。另外,這兩個字符一定要不同,不然會誤判“整個字符串都是回文串”的情況。
      
      	RP(i, 1, len){
      		if(i <= R)
      			r[i] = min(r[(mid << 1) - i], R - i);
      		else
      			r[i] = 1;
      		while(S[i - r[i]] == S[i + r[i]]) ++ r[i];
      		if(i + r[i] > R){
      			R = i + r[i];
      			mid = i;
      		}
      	}
      	int ans = 0;
      	RP(i, 1, len)
      		ans = max(ans, r[i]);
      	printf("%d", ans - 1);
      	return 0;
      }
      

      Z-algorithm

      又稱擴展KMP。可以快速處理出串\(S\)與其所有后綴的最長公共前綴。可以用KMP類似的方法求一個\(\text{nxt}\)數(shù)組,但是更簡單的方法還是利用manacher的思想。
      轉載一下原作者的博客鏈接:cosmicAC
      和manacher一樣,設\(r(i)\)表示從\(i\)開始的后綴和原串的最長公共前綴長度。和回文串一樣,當某一個lcp足夠大,足以覆蓋當前擴展點時,我們可以直接從前面繼承答案。設這個lcp的后綴起點為\(t\),和manacher類似,我們可以令\(r(i) = \min(r(l-t+1), t + r(t) - i)\),然后暴力擴展剩余未知的部分,直到找到第一個\(S_{r(i)} \neq S_{i+r(i)}\)\(r(i)\),然后更新\(t\)。似乎這個\(r(i)\)數(shù)組又稱“\(z\)數(shù)組”,這個算法才得名Z-algorithm。

      如何求文本串\(S\)與模式串\(T\)的各后綴的lcp?只需要把\(T\)與一個分隔符\(\Lambda\)與文本串\(S\)按順序連接成\(T + \Lambda + S\),再求\(T + \Lambda + S\)\(r(i)\)數(shù)組即可。答案為從分隔符后開始的\(r(i)\)
      另外,掃描要注意直接從\(1\)而不是\(0\)開始,原理應該和KMP在自匹配的過程一樣。

      int main(){
      	scanf("%s", S); int Sl = strlen(S);
      	scanf("%s", T); int Tl = strlen(T);
      	strcat(C, T), strcat(C, "$"), strcat(C, S);
      	len = strlen(C);
      	T[len + 1] = '@';
      	RP(i, 1, len - 1){
      		if(i <= r[t] + t)
      			r[i] = min(r[i - t], r[t] + t - i);
      		else
      			r[i] = 0;
      		while(C[i + r[i]] == C[r[i]])
      			++ r[i];
      		if(i + r[i] > t + r[t])
      			t = i;
      	}
      	RP(i, 0, Tl - 1) printf("%d ", i == 0 ? Tl : r[i]);
      	putchar('\n');
      	RP(i, Tl + 1, len - 1) printf("%d ", r[i]);
      	return 0;
      }
      

      另外,補充一下這個算法和普通KMP的轉換關系:

      if(i + r[i] > t + r[t]){
          RP(j, t + r[t], i + r[i] - 1) nxt[j] = j - i + 1;
          t = i;
      }
      

      Trie

      一種特殊的數(shù)據(jù)結構,用于維護若干個單詞。
      Trie有一種類似自動機的結構,可以通過字符指針\(p(q, c) = q\prime\)轉移到不同的狀態(tài),并可以標記終態(tài)以標識這個單詞的結尾。

      注意一下,Trie的空間復雜度為\(O(|\Sigma|N|S|)\),其中\(N\)為單詞數(shù),\(|\Sigma|\)為字符集大小,\(|S|\)為單詞平均長度。如果沒有算好空間,Trie樹很有可能會MLE。
      單次插入單詞和查詢單詞存在性的時間復雜度都是\(O(|S|)\)的。

      Trie還有一些神奇的應用。舉個例子:給定一個數(shù)集\(A_i\),求從中選出兩個數(shù),使得兩個數(shù)的異或和最大。最暴力的做法是直接\(O(N^2)\)比較,而通過Trie樹可以做到\(O(N \log A_{\max})\)。通過貪心,對于一個數(shù)\(x\),我們每次嘗試轉移到它的相反位;如果不能轉移,就妥協(xié)走另一邊。比如這道題:最長異或路徑。

      AC自動機

      本質上是在Trie樹上建立KMP自動機。但怎么建?這是一個大問題。
      仔細回憶一下KMP的實現(xiàn)方法:當遇到一個失配位置時,我們需要不斷迭代\(\text{nxt}\)指針,在保證前幾位匹配時,找到盡可能大的\(\text{nxt}\)位置。
      回憶一下,KMP\(\text{nxt}(i)\)數(shù)組的含義是“當前子串里,前綴和后綴的最長公共子串”。此時從\(S_1\)走到\(S_{\text{nxt}(i)}\)這個過程,和\(S_{i-\text{nxt}(i)+1}\)走到\(S_{i}\)完全等效。這個性質保證了我每次可以盡可能小地往回跳。

      AC自動機同理。\(fail\)指針使得“從某個狀態(tài)轉移到\(q\)狀態(tài)”,和“從根節(jié)點轉移到\(fail(q)\)狀態(tài)”完全等效。這使得我們失配之后有依可循。你可以把\(fail\)指針看成一條\(\varepsilon\)轉移邊,因為它的轉移不需要消耗任何字符。

      png

      如上圖所示。這兩條紫色的鏈條完全相同,但其中一個直接接在根節(jié)點上,而一個接在若干個點前。此時我們就可以用若干個\(fail\)指針連接兩條鏈。這樣,當當前指針失配時,我們可以隨時跳轉到另一條鏈上。

      如何連\(fail\)邊?假設整個trie樹的根節(jié)點為\(0\),那么對于任意一個\(p(0,c)\),它的失配指針只能指向\(0\)。這和KMP算法的初始化是一樣的。

      接下來,我們分層進行。對于一個節(jié)點\(u\),如果后繼狀態(tài)\(p(u,c)\)存在,那我們從后繼狀態(tài)連一條“平行”的\(fail\)邊指向另一條鏈,即令\(fail(p(u,c)) = p(fail(u),c)\)。這樣就可以形成向上圖那樣的分層網(wǎng)格的形狀了。
      如果\(p(u,c)\)不存在,那么我們直接把這個轉移邊和\(fail(u)\)的轉移邊合并,即令\(p(u,c) = p(fail(u),c)\)。這樣做的好處是可以形成一個\(trie\)圖,從而得以查詢任意長的字符串。

      但是,一個trie圖的結構過于復雜。舉個例子,單詞組\(\mathcal{hat},\mathcal{cat},\mathcal{cup}\)的trie圖如下:

      png

      事實上,從每個節(jié)點出發(fā)都有\(26\)條邊可走。青色的邊不是\(fail\)邊(在上例中,\(fail\)是全部指向\(0\)節(jié)點的),而是通過構造\(fail\)邊衍生出來的新的轉移邊。上圖省略了連向\(0\)的點,并且滿足青邊的轉移字符和轉移到這個狀態(tài)的紫邊的轉移字符一致。

      int p[maxn][26],leaf[maxn],tot,fail[maxn];
      void ins(char *str)
      {
      	int u=0;
      	int len=strlen(str);
      	RP(i,0,len-1)
      	{
      		int c=str[i]-'a';
      		if(!p[u][c])
      			p[u][c]=++tot;
      		u=p[u][c];
      	}
      	++leaf[u];
      }
      void prefail()
      {
      	RP(i,0,25)
      		if(p[0][i])
      			fail[p[0][i]]=0,q.push(p[0][i]);
      	while(!q.empty())
      	{
      		int u=q.front();q.pop();
      		RP(i,0,25)
      		{
      			if(p[u][i])
      				fail[p[u][i]]=p[fail[u]][i],q.push(p[u][i]);
      			else
      				p[u][i]=p[fail[u]][i];
      		}
      	}
      }
      int query(char *str)
      {
      	int len=strlen(str);
         	int r=0,ans=0;
      	RP(i,0,len-1)
         	{
      	   	int c=str[i]-'a';
      	   	r=p[r][c];
      	   	for(register int t=r;t&&~leaf[t];t=fail[t])
      	   		ans+=leaf[t],leaf[t]=-1;
      	}
      	return ans;
      }
      
      posted @ 2019-09-29 19:19  LinearODE  閱讀(285)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 布尔津县| 亚洲国产日韩伦中文字幕| 日韩高清亚洲日韩精品一区二区 | 亚洲综合国产伊人五月婷| 尉犁县| 国日韩精品一区二区三区| 美女自卫慰黄网站| 中文字幕亚洲制服在线看| 亚洲天堂网中文在线资源| 97人妻免费碰视频碰免| 国产精品久久毛片| 免费无码毛片一区二三区| 亚洲天堂网中文在线资源| 中文人妻熟妇乱又伦精品| 小污女小欲女导航| 日本国产精品第一页久久| 国产高潮刺激叫喊视频| 亚洲人精品午夜射精日韩| 高清性欧美暴力猛交| 成人啪啪高潮不断观看| 沂水县| 亚洲精品成人片在线观看精品字幕| 国产精品青草久久久久福利99| 制服jk白丝h无内视频网站| 国产精品色哟哟成人av| 成人性生交大片免费看中文| 精品无码一区在线观看| 国产av剧情无码精品色午夜| 久久精品国产亚洲av麻豆小说| 国精品午夜福利视频| 精品国产乱码久久久久app下载| 亚洲av无码专区在线亚| 亚洲熟女乱色综合亚洲图片| 午夜国产理论大片高清| 产综合无码一区| 91福利国产午夜亚洲精品| 99久久亚洲综合精品成人网| 国产成人高清亚洲综合| 韩国19禁无遮挡啪啪无码网站| 亚洲欧美日韩在线码| 亚洲综合精品成人|