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