2017清北學堂(提升組精英班)集訓筆記——字符串算法

啊~個人速度真的是太慢了,學校又要提早開學!!還有兩個月就要比賽了,垂死掙扎一下吧~html

繼續更新筆記(眼含淚水)正則表達式

1、Trie樹:算法

1.定義:經過字符串建成一棵樹,這棵樹的節點個數必定是最少的。例如:4個字符串"ab","abc","bd","dda"對應的trie樹以下:數組

其中紅色節點表示存在一個字符串是以這個點結尾的。oop

一個性質:在樹上,兩個點u,v知足u是v的祖先,那麼u表明的字符串必定是v表明的字符串的前綴。網站

2.Trie樹的插入:能夠從根節點出發,每次沿着要走的字符串往下走,若沒有則創建新節點。spa

假如全部字符串的長度之和爲n,構建這棵trie樹的時間複雜度爲O(n)。指針

 1 int root=1;
 2 int cnt=1;
 3 int p[i][j];//表示從i這個節點沿着j這個字符走,能走到哪一個點,走不到就是0
 4 char s[];//存儲字符串 
 5 for(int i=1;i<=n;i++)
 6 {
 7     scanf("%s",s);
 8     int len=strlen(s);
 9     int now=root;//now表示走到的當前節點,now的初始值爲根節點
10     for(int j=0;j<len;j++)
11     {
12         if(p[now][s[j]]>0){
13             now=p[now][s[j]];//若是能沿着j節點往下走,直接往下走
14         }
15         else{
16             pow[now][s[j]]=++cnt;
17             now=p[now][s[j]];
18         } 
19     } 
20     v[now]++;//記錄now這個節點被訪問的次數 
21 } 

3.Trie樹的查詢:能夠看出trie樹中每一個節點表示其中一個字符串的前綴,在作題過程當中每每經過這個性質來獲得較好的時間複雜度。code

4.一個例題:給定n個互不相同的串,求存在多少對數(i,j)(共n2對)知足第i個串是第j個串的前綴。htm

全部串的長度之和≤500000。

解題根據性質,「在樹上,兩個點u,v知足u是v的祖先,那麼u表明的字符串必定是v表明的字符串的前綴」。

咱們要知足一個串是另外一個串的前綴,也就是說,在trie樹上,這個串對應的位置是另外一個串對應的位置的祖先

構建這棵trie樹,而後咱們枚舉每一個紅色點,它對答案的貢獻是以它爲根的子樹中紅色節點的個數之和。這個東西能夠在一開始遍歷這棵樹預處理出來!

時間複雜度是線性的。

 1 void dfs(int x)
 2 {
 3     if(v[x]) sum[x]++;
 4     for(char i='a';i<='z';i++)
 5     {
 6         if(p[x][i])//從當前x沿着i這個字符走還能往下走 
 7         {
 8             dfs(p[x][i]);//往下走 
 9             sum[x]+=sum[p[x][i]];//累加貢獻sum 
10         }
11     }
12 } 
13 dfs(1);//從根節點開始 

5.USACO的某題(這題真的亂)給定n個串,重排字符之間的大小關係,問哪些串有可能成爲字典序最小的串。

全部字符串的長度之和<=100000。

例如,有一個字符串,裏面只有'a'~'z'這些字符,默認地,'a'是最小的,'z'是最大的。可是咱們能夠從新定義字符間的大小關係,好比這樣:b<c<d<y<x<z,從而咱們對於一些字符串,就按照咱們新定義的大小關係來比較字典序大小。

舉個栗子

三個字符串"aab","aba","baa",總共只出現了兩個字符'a'和'b',因此字符間的大小關係要麼是a<b要麼是a>b,假設a<b,則第一個串"aab"就是字典序最小的串;假設b<a,則第三個串"baa"就是字典序最小的串,可是對於第二個串,不管咱們怎麼定義字符間的大小關係,都不可能成爲三個字符串中字典序最小的。

解題:首先對這n個串構建trie樹,以後對每一個串,從根走向它,這個路程中遇到的全部兄弟在字典序下都比它大

對於每一個串,從前日後,直接肯定這個字符的大小關係,判斷是不是字典序最小

如下兩個串都是有可能成爲字典序最小的串的:

abcd…xyza

abcd…xyzb

因此有:u是v的前綴,則v必定不是字典序最小的串。

如今問題來了:對於每一個串,在什麼條件下,是字典序最小的??

來個栗子:有一些字符串("aabc","aac","aad","ac","adb","aabb"),構造trie樹以下:

對於"aabc"這個串,往下遍歷:

從深度爲2的那一層,咱們能夠獲得:a<c,a<d;

從深度爲3的那一層,咱們能夠獲得:b<c,b<d;

從深度爲4的那一層,咱們還能夠獲得:c<b。

綜上所述:咱們獲得了一堆的關係:a<c,a<d;b<c,b<d;c<b,容易看出,這些關係是存在矛盾的,因此無解->"aabc"是不可能成爲字典序最小的串。

因此要想有解,這一堆的關係必須知足兩個條件:①不存在矛盾②不存在環

總的來講,就是判斷這一堆的關係是否存在拓撲序

最多有26×26個大小關係,而最多有100000個字符串,因此時間複雜度最大爲:O(26×26×100000)

2、KMP算法(「看mao片算法」~咳咳):

給定兩個字符串A,B,判斷T是否爲S的子串(變式:尋找子串B在串A中的位置)。

要求一個O(|A|+|B|)的作法。

一般稱A爲目標串(或主串),B爲模式串。

算法過程

咱們假設串A的長度爲n,串B的長度爲m,每一個字符串的開頭下標默認爲1。

定義兩個變量i和j,這兩個變量共同表示:A[i-j+1~i]與B[1~j]均匹配,即:A中以第i個字符結尾的、長度爲j的字符串,和B從頭開始長度爲j的字符串徹底匹配

繼續往下匹配:若是i+1和j+1不匹配。

如今,就是用到了KMP算法的核心:它對這一狀況的處理方式是減小j,就至關於將子串向右平移

平移的目的是爲了讓「A[i-j+1~i]與B[1~j]均匹配」這個條件從新知足。

在上圖中,j一直減少到了0,由於向右平移的過程當中,始終不能讓這個條件知足(最右邊"?"部分已經越界)

但有時候,將j減小一點點以後,是能夠從新知足條件的,例如:

那麼咱們將j從7減少到4時,有:

這樣就能夠徹底匹配啦!可是後面還有沒有匹配的機會咱們就無論了,至少咱們已經保證A[4~7]和B[1~4]徹底匹配上了。

如今考慮一個問題:咱們每次把j減少1(一位一位地平移B字符串),這樣太慢了,咱們在這裏預處理一個next[]數組,表示當j匹配不下去的時候,咱們能夠把j減小到next[j],繼續嘗試匹配

預處理過程:讓j本身和本身匹配一下,一旦匹配發現B[k-m+1~k] 和 B[1~m] 匹配,則說明在A與B匹配過程當中,j等於k匹配不下去時,j能夠嘗試減少到m。

過程以下:


/**************************************///靚麗的分界線

一些代碼:

 1 /*核心內容*/
 2 for(int i=1,j=0;i<=n;i++)
 3 {
 4     while(j&&B[j+1]!= A[i]) j=next[j];
 5     if(B[j+1]==A[i]) j++;
 6     if(j==m)
 7     {
 8         printf("%d\n", i-j+1);//輸出找到的"B字串在 A中位置"
 9         //若是要求的是出現次數,這裏也有多是ans++什麼的
10         j=next[j];//讓循環進行下去
11     }
12 }
1 for(int i=2,j=0;i<=m;i++)/*預處理next[]數組*/ 2 {
3     while(j&&B[j+1]!=B[i]) j=next[j];
4     if(B[j+1]==B[i]) j++;
5     next[i]=j;
6 }

經典例題:Blue jeans(POJ 3080)

給定m個串,求字典序最小的公共子串。找一個串,使得這個串是全部串的子串,而且字典序最小。

m≤10,每一個串的長度≤60.

解題思路:一個串,若是是全部串的子串,那麼確定是第一個串的子串。

枚舉全部子串,複雜度爲60*60。
驗證其它串是否包含這個子串,複雜度爲10*60。
每次更新答案便可。

時間複雜度爲:O(603×10)

經典例題:Seek the Name,Seek the Fame(POJ 2752)

給定一個字符串S,求全部既是S的前綴又是S的後綴的子串,從小到大輸出這些串的長度。
|S|<=500000。

N爲字符串S的長度。

解題思路

回到KMP算法,咱們令P[j]表示找最大的數x,使得B中位置是1~x的字符與j-x+1~j的字符徹底相同,也就是上面講的KMP算法中的next[]。

考慮P[|S|]的意義,也就是最大的前綴等於後綴的長度(不包括其自己)。

那P[P[|S|]]就是次大的。
所以全部P[P[…P[|S|]]]就是答案了,一直這樣遞歸下去就能夠找到答案。

或者用Hash來作,這樣更容易想到也比較方便,但效率沒有KMP高。

3、AC自動機:

有n個模式串,長度之和是|T|,有一個主串,長度是|S|,問哪些模式串是這個主串的子串(或者有多少個模式串在主串中出現過)?

解法一:直接跑n次KMP算法,時間複雜度:O(n×|S|)。

解法二:AC自動機,時間複雜度:O(|T|+|S|),對於n個串,構建trie樹,在trie樹上作KMP

在這裏我來詳解一下AC自動機啊~

首先咱們定義一個指針,叫作「失配指針」或者「失敗指針」,在KMP算法中,這個失配指針就是next[]數組,一樣地在AC自動機中,在trie樹上也定義一個失配指針與此相似但不徹底相同。

失配指針:假設一個節點k的失配指針指向j,那麼k、j知足性質:設根節點root到j的距離爲n,則從k以上的第n個節點到k這個節點所組成的長度爲n的單詞,與根節點root到j所組成的單詞徹底相同。

以下圖:

單詞"she"中的'e'的失配指針指向的是單詞"her"中的'e',由於紅框中的部分是徹底同樣的。

而後,問題來了,咱們該怎樣處理這個失配指針呢?其實咱們能夠用BFS就很方便地解決了。

處理過程:讓和根節點直接相連的節點的失配指針指向根節點,對於其餘節點(假設爲a),設這個節點上的字母爲ch,沿着a的父親b的失配指針走,一直走到一個節點c,c的兒子中也有字母爲ch的節點d,而後把a節點的失配指針指向c節點的兒子d(由於d的字母也爲ch),若是一直走到了根節點都沒找到,那就把失配指針指向根節點。

最開始,咱們把根節點加入隊列(根節點的失敗指針顯然指向本身),這之後咱們每處理一個點,就把它的全部兒子加入隊列,直到搞完。

這樣咱們就獲得了一棵帶有失配指針的trie樹了,接下來正式介紹AC自動機工做原理!

AC自動機原理:對於一棵trie樹,咱們用黃色表示一個單詞(某個模式串)的末尾,也就是說從根節點走到一個黃色的點,就組成一個「單詞」,以下圖:

一開始,trie樹中有一個指針t1指向根節點root,將這n個模式串合併爲一個模式主串,模式主串中有一個指針t2指向這個模式主串的串頭。

接下來進行相似KMP算法的操做:若是t2指向的字母,是trie樹中,t1指向的節點的兒子,那麼把t2+1,t1改成那個兒子的編號,不然t1順這當前節點的失配指針往上找,直到t2是t1的一個兒子,或者t1指向根爲止。

若是t1通過了一個黃色的點,那麼以這個點結尾的單詞就算出現過了(這個模式串已經在主串中出現了),或者若是t1所在的點能夠沿着失配指針走到一個黃色的點,那麼以那個黃色的點爲結尾的單詞就算出現過了(這個模式串已經在主串中出現了),記錄答案便可。

代碼。。。我比較懶並且弱,之後有機會再發吧!

經典例題:僞裝是字符串的題——正則表達式(ZHW YY出來的題)

給定一個字符串,判斷其是否爲合法的正則表達式。

一個正則表達式定義爲:

①0是正則表達式,1也是正則表達式。

②P和Q都是正則表達式,則PQ也是正則表達式。

③P是正則表達式,則(P)是正則表達式

④P是正則表達式,則P*也是正則表達式

⑤P和Q都是正則表達式,則P|Q是正則表達式

舉個栗子:

010101101*
(11|0*)*

以上都是都是正則表達式
|S|<=100

解題思路:令dp[i][j]表示第i個字符到第j個字符可否組成正則表達式,分5種狀況進行轉移就能夠了。

四:對拍方法:

對拍:你給兩個程序,和一個隨機數據生成器,而後系統去用這個隨機數據生成器的輸出做爲你這兩個程序的輸入,而後比較你這兩個程序的輸出,能夠找到一組使這兩個程序輸出不同的數據(若是存在的話),這樣就能夠提升正確率。

通常的對拍是:對於一個題,咱們寫一個暴力算法,管它什麼時間空間效率呢?!而後寫一個你對於這題的正確解法程序,拿這兩個程序進行對拍。

對拍的實現過程

首先,在本地新建一個文件夾。

而後,在裏面放入std.exe、mkdt.exe、a.exe這三個exe程序(名字能夠本身亂取)。

std.exe:你暴力寫的一個作法或者你從網上找的一份AC代碼生成的程序,反正結果確定是對的。

a.exe:你的代碼生成的程序,你不知道他對不對或者你知道他是WA的可是你不知道哪裏WA了。

mkdt.exe:就是你的隨機數據生成器,你能夠用它去生成你認爲的合法數據。

最後,在文件夾中新建一個txt記事本文檔,在裏面輸入如下代碼:

1 :loop
2     mkdt.exe
3     std.exe
4     a.exe
5     fc std.out a.out
6     if %errorlevel%==0 goto loop
7 pause

而後保存,把".txt"這個後綴名改成".bat",雙擊運行便可!

5、隨機算法:

①隨機生成一棵樹:

1 for(int i=2;i<=n;i++)/*隨機生成一棵樹*/
2 {
3     cout<<rand()%(i-1)+1<<' '<<i<<endl;
4 }//深度爲lgn 

②隨機生成一棵長毛的鏈:

1 /*隨機生成一棵長毛的鏈:1~n/2*/
2 for(int i=2;i<=n/2;i++) cout<<i-1<<' 'i<<endl;
3 for(int i=n/2+1;i<=n;i++) cout<<rand()%(i-1)+1<<' '<<endl; 

③給你一張圖,生成一張圖:

 1 /*給你一張圖,生成一張圖,10萬個點,20萬條邊 */
 2 map<long long,int> mp;
 3 for(int i=1;i<=200000;i++)
 4 {
 5     int A=rand()%n+1;
 6     int B=rand()%n+1;
 7     while(A==B||mp[1ll*A*100005+B])
 8     {
 9         A=rand()%n+1;
10         B=rand()%n+1;
11     }
12     mp[1ll*A*10005+B]=1;
13     cout<<A<<' '<<B<<endl;
14 } 

④隨機生成一個連通圖:

先生成一棵樹,這棵樹上的邊是必定存在的,在隨機其餘的邊。

最近發現一些網站盜用個人blog,這實在不能忍(™把關於個人名字什麼的所有刪去只保留文本啥意思。。)!!但願各位轉載引用時請註明出處,謝謝配合噢~

原博客惟一地址:http://www.cnblogs.com/geek-007/p/7296453.html

相關文章
相關標籤/搜索