字符串相关
文章目錄
- 字符串基礎
- 字符串的存儲
- 標準庫
- 字符串匹配
- 單串匹配
- 多串匹配
- 其他類型的字符串匹配問題
- 字符串哈希
- Hash 的實現
- Hash 的分析與改進
- 錯誤率
- 多次詢問子串哈希
- Hash 的應用
- 字符串匹配
- 允許 k次失配的字符串匹配
- 最長回文子串
- 最長公共子字符串
- 確定字符串中不同子字符串的數量
- 字典樹 (Trie)
- 應用
- 檢索字符串
- AC 自動機
- 維護異或極值
- AC 自動機
- Manacher
字符串基礎
字符串的存儲
標準庫
C 標準庫是在對字符數組進行操作:char[]/const char*
| strlen(const char *str) | 返回從 str[0] 開始直到 ‘\0’ 的字符數。注意,未開啟 O2 優化時,該操作寫在環條件中復雜度是 O(n)O(n)O(n)的。 |
| printf("%s", s) | 用 %s 來輸出一個字符串(字符數組)。 |
| scanf("%s", &s) | 用 %s 來讀入一個字符串(字符數組)。 |
| sscanf(const char *__source, const char *__format, …) | 從字符串 __source 里讀取變量,比如 sscanf(str,"%d",&a)。 |
| sprintf(char *__stream, const char *__format, …) | 將 __format 字符串里的內容輸出到 __stream 中,比如 sprintf(str,"%d",i)。 |
| strcmp(const char *str1, const char *str2) | 按照字典序比較 str1 str2 若 str1 字典序小返回負值,兩者一樣返回 0,str1 字典序更大則返回正值。請注意,不要簡單的認為返回值只有0 ,1,-1 三種,在不同平臺下的返回值都遵循正負,但并非都是 0,-1,1。 |
| strcpy(char *str, const char *src) | 把 src 中的字符復制到 str 中,str src 均為字符數組頭指針,返回值為 str 包含空終止符號 ‘\0’。 |
| strncpy(char *str, const char *src, int cnt) | 復制至多 cnt 個字符到 str 中,若 src 終止而數量未達 cnt 則寫入空字符到 str 直至寫入總共 cnt 個字符。 |
| strcat(char *str1, const char *str2): | 將 str2 接到 str1 的結尾,用 *str2 替換 str1 末尾的 ‘\0’ 返回 str1。 |
| strstr(char *str1, const char *str2) | 若 str2 是 str1 的子串,則返回 str2 在 str1 的首次出現的地址;如果 str2 不是 str1 的子串,則返回 NULL。 |
| strchr(const char *str, int c) | 找到在字符串 str 中第一次出現字符 c 的位置,并返回這個位置的地址。如果未找到該字符則返回 NULL。 |
| strrchr(const char *str, char c) | 找到在字符串 str 中最后一次出現字符 c 的位置,并返回這個位置的地址。如果未找到該字符則返回 NULL。 |
C++ 標準庫是在對字符串對象進行操作,同時也提供對字符數組的兼容。 std::string
| 重載了賦值運算符 + | 當 + 兩邊是 string/char/char[]/const char* 類型時,可以將這兩個變量連接,返回連接后的字符串(string)。 |
| 賦值運算符 = | 右側可以是 const string/string/const char*/char*。 |
| 訪問運算符 [cur] | 返回 cur 位置的引用。 |
| 訪問函數 data()/c_str() | 返回一個 const char* 指針,內容與該 string 相同。 |
| 容量函數 size() | 返回字符串字符個數。 |
| find(ch, start = 0) | 查找并返回從 start 開始的字符 ch 的位置;rfind(ch) 從末尾開始,查找并返回第一個找到的字符 ch 的位置(皆從 0開始)(如果查找不到,返回 -1)。 |
| substr(start, len) | 可以從字符串的 start(從 0開始)截取一個長度為 len 的字符串(缺省 len 時代碼截取到字符串末尾)。 |
| append(s) | 將 s 添加到字符串末尾。 |
| append(s, pos, n) | 將字符串 s 中,從 pos 開始的 n 個字符連接到當前字符串結尾。 |
| replace(pos, n, s) | 刪除從 pos 開始的 n 個字符,然后在 pos 處插入串 s。 |
| erase(pos, n) | 刪除從 pos 開始的 n 個字符。 |
| insert(pos, s) | 在 pos 位置插入字符串 s。 |
| std::string | 重載了比較邏輯運算符,復雜度是 O(n)的。 |
字符串匹配
單串匹配
一個模式串 (pattern),一個待匹配串,找出前者在后者中的所有出現位置
舉例:Oulipo HDU - 1686(哈希或KMP)匹配字符串
多串匹配
多個模式串,一個待匹配串(多個待匹配串可以直接連起來)。
直接當做單串匹配肯定是可以的,但是效率不夠高。
舉例:Keywords Search HDU - 2222(AC自動機模板)
其他類型的字符串匹配問題
例如匹配一個串的任意后綴、匹配多個串的任意后綴等。
字符串哈希
Hash 的核心思想在于,將輸入映射到一個值域較小、可以方便比較的范圍。
Warning
這里的“值域較小”在不同情況下意義不同。
在 哈希表 中,值域需要小到能夠接受線性的空間與時間復雜度。
在字符串哈希中,值域需要小到能夠快速比較(10910^9109、101810^{18}1018 都是可以快速比較的)。
同時,為了降低哈希沖突率,值域也不能太小。
我們定義一個把字符串映射到整數的函數 fff,這個fff 稱為是 Hash 函數。
我們希望這個函數 fff 可以方便地幫我們判斷兩個字符串是否相等。
具體來說,哈希函數最重要的性質可以概括為下面兩條:
Hash 函數值一樣時原字符串卻不一樣的現象我們成為哈希碰撞。
我們需要關注的是什么?
時間復雜度和 Hash 的準確率。
通常我們采用的是多項式 Hash 的方法,對于一個長度為 lll 的字符串 s來說,我們可以這樣定義多項式 Hash 函數:f(s)=∑i=1ls[i]×bl?i(modf(s)=\sum^{l}_{i=1}s[i]\times b^{l-i}(modf(s)=∑i=1l?s[i]×bl?i(mod M)M)M)。例如,對于字符串xyzxyzxyz ,其哈希函數值為xb2+yb+zxb^2+yb+zxb2+yb+z 。
特別要說明的是,也有很多人使用的是另一種 Hash 函數的定義,即f(s)=∑i=1ls[i]×bi?1(modf(s)=\sum^{l}_{i=1}s[i]\times b^{i-1}(modf(s)=∑i=1l?s[i]×bi?1(mod M)M)M) ,這種定義下,同樣的字符串 xyzxyzxyz的哈希值就變為了 x+by+zb2x+by+zb^2x+by+zb2 了。顯然,上面這兩種哈希函數的定義函數都是可行的,但二者在之后會講到的計算子串哈希值時所用的計算式是不同的,因此千萬注意 不要弄混了這兩種不同的 Hash 方式。由于前者的 Hash 定義計算更簡便、使用人數更多、且可以類比為一個 b 進制數來幫助理解,所以本文下面所將要討論的都是使用 f(s)=∑i=1ls[i]×bl?i(modf(s)=\sum^{l}_{i=1}s[i]\times b^{l-i}(modf(s)=∑i=1l?s[i]×bl?i(mod M)M)M) 來定義的 Hash 函數。
下面講一下如何選擇 M和計算哈希碰撞的概率。
這里 M 需要選擇一個素數(至少要比最大的字符要大),b 可以任意選擇。如果我們用未知數 x 替代b ,那么f(x)f(x)f(x) 實際上是多項式環ZM[x]\mathbb{Z}_{M}[x]ZM?[x] 上的一個多項式。考慮兩個不同的字符串 s,t有f(s)=f(t)f(s)=f(t)f(s)=f(t) 。我們記h(x)=f(s)?f(t)=∑i=1l(s[i]?t[i])xl?i(modh(x)=f(s)-f(t)=\sum^{l}_{i=1}(s[i]-t[i])x^{l-i}(modh(x)=f(s)?f(t)=∑i=1l?(s[i]?t[i])xl?i(mod M)M)M) ,其中l=max(∣s∣,∣t∣)l=max(|s|,|t|)l=max(∣s∣,∣t∣) 。可以發現 h(x) 是一個 l?1l-1l?1 階的非零多項式。如果 s與t 在x=b 的情況下哈希碰撞,則 b是h(x) 的一個根。由于 h(x) 在ZM\mathbb{Z}_{M}ZM? 是一個域(等價于 M 是一個素數,這也是為什么 M 要選擇素數的原因)的時候,最多有l?1l-1l?1 個根,如果我們保證 b 是從 [0,M) 之間均勻隨機選取的,那么 f(s)f(s)f(s) 與 f(t)f(t)f(t) 碰撞的概率可以估計為 l?1M\frac{l-1}{M}Ml?1?。簡單驗算一下,可以發現如果兩個字符串長度都是 1 的時候,哈希碰撞的概率為 1?1M\frac{1-1}{M}M1?1?=0,此時不可能發生碰撞。
Hash 的實現
參考代碼:(效率低下的版本,實際使用時一般不會這么寫)
using std::string;const int M = 1e9 + 7; const int B = 233;typedef long long ll;int get_hash(const string& s) {int res = 0;for (int i = 0; i < s.size(); ++i) {res = (ll)(res * B + s[i]) % M;}return res; }bool cmp(const string& s, const string& t) {return get_hash(s) == get_hash(t); }Hash 的分析與改進
錯誤率
若進行 n次比較,每次錯誤率 1M\frac{1}M{}M1?,那么總錯誤率是1?(1?1M)n1-(1-\frac{1}{M})^{n}1?(1?M1?)n。在隨機數據下,若M=109+7M=10^{9}+7M=109+7 ,n=106n=10^6n=106,錯誤率約為 11000\frac{1}{1000}10001?,并不是能夠完全忽略不計的。
所以,進行字符串哈希時,經常會對兩個大質數分別取模,這樣的話哈希函數的值域就能擴大到兩者之積,錯誤率就非常小了。
多次詢問子串哈希
單次計算一個字符串的哈希值復雜度是O(n) ,其中 n為串長,與暴力匹配沒有區別,如果需要多次詢問一個字符串的子串的哈希值,每次重新計算效率非常低下。
一般采取的方法是對整個字符串先預處理出每個前綴的哈希值,將哈希值看成一個b 進制的數對M 取模的結果,這樣的話每次就能快速求出子串的哈希了:
令fi(s)f_{i}(s)fi?(s) 表示 f(s[1...i])f(s[1...i])f(s[1...i]),即原串長度為 iii 的前綴的哈希值,那么按照定義有 fi(s)=s[1]×bi?1+s[2]×bi?2+...+s[i?1]×b+s[i]f_i(s)=s[1]\times b^{i-1}+s[2]\times b^{i-2}+...+s[i-1]\times b+s[i]fi?(s)=s[1]×bi?1+s[2]×bi?2+...+s[i?1]×b+s[i]
現在,我們想要用類似前綴和的方式快速求出f(s[l...r])f(s[l...r])f(s[l...r]) ,按照定義有字符串 s[l...r]s[l...r]s[l...r]的哈希值為 f(s[l...r])=s[l]×br?l+s[l+1]×br?l?1+...+s[r?l]×b+s[r]f(s[l...r])=s[l]\times b^{r-l}+s[l+1]\times b^{r-l-1}+...+s[r-l]\times b+s[r]f(s[l...r])=s[l]×br?l+s[l+1]×br?l?1+...+s[r?l]×b+s[r]
對比觀察上述兩個式子,我們發現 f(s[l...r])=fr(s)?fl?1(s)×br?l+1f(s[l...r])=f_r(s)-f_{l-1}(s)\times b^{r-l+1}f(s[l...r])=fr?(s)?fl?1?(s)×br?l+1 成立,因此我們用這個式子就可以快速得到子串的哈希值。其中br?l+1b^{r-l+1}br?l+1 可以O(n) 的預處理出來然后O(1) 的回答每次詢問(當然也可以快速冪 O(log n)的回答每次詢問)。
Hash 的應用
字符串匹配
求出模式串的哈希值后,求出文本串每個長度為模式串長度的子串的哈希值,分別與模式串的哈希值比較即可。
允許 k次失配的字符串匹配
問題:給定長為 n 的源串 ,以及長度為m 的模式串 ,要求查找源串中有多少子串與模式串匹配。s′s's′ 與s 匹配,當且僅當 s′s's′與s 長度相同,且最多有 k 個位置字符不同。其中 1≤n,m≤1061\leq n,m\leq 10^61≤n,m≤106,0≤k≤50\leq k\leq 50≤k≤5。
這道題無法使用 KMP 解決,但是可以通過哈希 + 二分來解決。
枚舉所有可能匹配的子串,假設現在枚舉的子串為s′s's′ ,通過哈希 + 二分可以快速找到 s′s's′ 與p 第一個不同的位置。之后將 s′s's′ 與 p 在這個失配位置及之前的部分刪除掉,繼續查找下一個失配位置。這樣的過程最多發生 k 次。總的時間復雜度為O(m+knO(m+knO(m+kn log2log_2log2? m)m)m) 。
最長回文子串
二分答案,判斷是否可行時枚舉回文中心(對稱軸),哈希判斷兩側是否相等。需要分別預處理正著和倒著的哈希值。時間復雜度O(nO(nO(n logloglog n)n)n) 。
這個問題可以使用 manacher 算法 在 O(n)O(n)O(n) 的時間內解決。
通過哈希同樣可以O(n)O(n)O(n) 解決這個問題,具體方法就是記 RiR_{i}Ri? 表示以 iii 作為結尾的最長回文的長度,那么答案就是maxi=1nRimax^{n}_{i=1}R_{i}maxi=1n?Ri? 。考慮到 Ri≤Ri?1+2R_i\leq R_{i-1}+2Ri?≤Ri?1?+2,因此我們只需要暴力從 Ri?1+2R_{i-1}+2Ri?1?+2開始遞減,直到找到第一個回文即可。記變量 zzz 表示當前枚舉的 RiR_iRi?,初始時為0 ,則 zzz 在每次 iii 增大的時候都會增大 2,之后每次暴力循環都會減少1 ,故暴力循環最多發生2n 次,總的時間復雜度為 O(n)。
最長公共子字符串
問題:給定 m 個總長不超過n 的非空字符串,查找所有字符串的最長公共子字符串,如果有多個,任意輸出其中一個。其中1≤m,n≤1061\leq m,n\leq10^61≤m,n≤106 。
很顯然如果存在長度為k 的最長公共子字符串,那么 k-1 的公共子字符串也必定存在。因此我們可以二分最長公共子字符串的長度。假設現在的長度為k ,check(k) 的邏輯為我們將所有所有字符串的長度為k 的子串分別進行哈希,將哈希值放入 n 個哈希表中存儲。之后求交集即可。
時間復雜度為O(nO(nO(n log2log_2log2? nm)\frac{n}{m})mn?) 。
確定字符串中不同子字符串的數量
問題:給定長為n 的字符串,僅由小寫英文字母組成,查找該字符串中不同子串的數量。
為了解決這個問題,我們遍歷了所有長度為 l=1,...,nl=1,...,nl=1,...,n 的子串。對于每個長度為 lll,我們將其 Hash 值乘以相同的 b 的冪次方,并存入一個數組中。數組中不同元素的數量等于字符串中長度不同的子串的數量,并此數字將添加到最終答案中。
為了方便起見,我們將使用 h[i]h[i]h[i] 作為 Hash 的前綴字符,并定義h[0]=0h[0]=0h[0]=0 。
字典樹 (Trie)
字典樹,英文名 trie。顧名思義,就是一個像字典一樣的樹。
先放一張圖:
可以發現,這棵字典樹用邊來代表字母,而從根結點到樹上某一結點的路徑就代表了一個字符串。舉個例子,1→4→8→121\rightarrow4\rightarrow8\rightarrow121→4→8→12 表示的就是字符串 caa。
trie 的結構非常好懂,我們用 δ(u,c)\delta(u,c)δ(u,c) 表示結點uuu 的 ccc 字符指向的下一個結點,或著說是結點 uuu 代表的字符串后面添加一個字符 ccc 形成的字符串的結點。( ccc 的取值范圍和字符集大小有關,不一定是 000~26。)
有時需要標記插入進 trie 的是哪些字符串,每次插入完成時在這個字符串所代表的節點處打上標記即可。
Phone List POJ - 3630(字典樹模板題)
應用
檢索字符串
字典樹最基礎的應用——查找一個字符串是否在“字典”中出現過。
例題:字典樹模板+洛谷P2580 于是他錯誤的點名開始了
AC 自動機
trie 是 AC 自動機 的一部分。
維護異或極值
將數的二進制表示看做一個字符串,就可以建出字符集為{0,1} 的 trie 樹。
前綴函數與 KMP 算法
Boyer-Moore算法
Z 函數(擴展 KMP)
自動機
AC 自動機
Keywords Search HDU - 2222(AC自動機模板)
后綴數組 (SA)
后綴自動機 (SAM)
后綴平衡樹
廣義后綴自動機
后綴樹
Manacher
最長回文 HDU - 3068(求最長回文串的長度【馬拉車算法Manacher】)
回文樹
序列自動機
最小表示法
Lyndon 分解
總結
- 上一篇: 线段树维护区间最大值+第 45 届(IC
- 下一篇: 吃凉拌苦瓜可以减肥吗