OSDN Git Service

:construction: update mkdocs.yml, index, hash, match; add automaton
authorouuan <y___o___u@126.com>
Wed, 11 Sep 2019 00:30:55 +0000 (08:30 +0800)
committerouuan <y___o___u@126.com>
Wed, 11 Sep 2019 00:30:55 +0000 (08:30 +0800)
docs/string/automaton.md
docs/string/general-sam.md [new file with mode: 0644]
docs/string/hash.md
docs/string/images/automaton1.png [new file with mode: 0644]
docs/string/index.md
docs/string/match.md
docs/string/seq-automaton.md [moved from docs/string/sequence-am.md with 100% similarity]
mkdocs.yml

index 8b13789..69d41e3 100644 (file)
@@ -1 +1,87 @@
+在 OI 中,我们所说的“自动机”一般都指“确定有限状态自动机”。
 
+## 定义
+
+一个**确定有限状态自动机(DFA)**由以下五部分构成:
+
+1. **字符集**($\Sigma$),该自动机只能输入这些字符。
+2. **状态集合**($Q$)。如果把一个 DFA 看成一张有向无环图(DAG),那么 DFA 中的状态就相当于图上的顶点。
+3. **起始状态**($start$),$start\in Q$,是一个特殊的状态。起始状态一般用 $s$ 表示,为了避免混淆,本文中使用 $start$。
+4. **接受状态集合**($F$),$F\subseteq Q$,是一堆特殊的状态。
+5. **转移函数**($\delta$),$\delta$ 是一个接受两个参数返回一个值的函数,其中第一个参数和返回值都是一个状态,第二个参数是字符集中的一个字符。如果把一个 DFA 看成一张有向无环图(DAG),那么 DFA 中的转移函数就相当于顶点间的边,而每条边上都有一个字符。
+
+DFA 的作用就是识别字符串,一个自动机 $A$,若它能识别(接受)字符串 $S$,那么 $A(S)=True$,否则 $A(S)=False$。
+
+当一个 DFA 读入一个字符串时,从初始状态起按照转移函数一个一个字符地转移。如果读入完一个字符串的所有字符后位于一个接受状态,那么我们称这个 DFA **接受** 这个字符串,反之我们称这个 DFA **不接受** 这个字符串。
+
+如果一个状态 $v$ 没有字符 $c$ 的转移,那么我们令 $\delta(v,c)=null$,而 $null$ 只能转移到 $null$,且 $null$ 不属于接受状态集合。无法转移到任何一个接受状态的状态都可以视作 $null$,或者说,$null$ 代指所有无法转移到任何一个接受状态的状态。
+
+我们扩展定义转移函数 $\delta$,令其第二个参数可以接收一个字符串:$\delta(v,s)=\delta(\delta(v,s[0]),s[1..|s|-1])$,这个扩展后的转移函数就可以表示从一个状态起接收一个字符串后转移到的状态。那么,$A(s)=[\delta(start,s)\in F]$。
+
+如,一个接受且仅接受字符串 $a,ab,aac$ 的 DFA:
+
+![](./images/automaton1.png)
+
+## OI 中常用的自动机
+
+### 字典树
+
+[字典树](./trie.md) 是大部分 OIer 接触到的第一个自动机,接受且仅接受指定的字符串集合中的元素。
+
+转移函数就是 Trie 上的边,接受状态是将每个字符串插入到 Trie 时到达的那个状态。
+
+### KMP 自动机
+
+[KMP 算法](./kmp.md) 可以视作自动机,基于字符串 $s$ 的 KMP 自动机接受且仅接受以 $s$ 为后缀的字符串,其接受状态为 $|s|$。
+
+转移函数:
+$$
+\delta(i, c)=
+\begin{cases}
+i+1&s[i+1]=c\\
+\delta(\pi(i),c)&s[i+1]\ne c
+\end{cases}
+$$
+(需要特别定义 $\forall c\in\Sigma,\delta(\pi(1),c)=1$)
+
+### AC 自动机
+
+[AC 自动机](./ac-automaton.md) 接受且仅接受以指定的字符串集合中的某个元素为后缀的字符串。也就是 Trie + KMP。
+
+### 后缀自动机
+
+[后缀自动机](./sam.md) 接受且仅接受指定字符串的后缀。
+
+### 广义后缀自动机
+
+[广义后缀自动机](./general-sam.md) 接受且仅接受指定的字符串集合中的某个元素的后缀。也就是 Trie + SAM。
+
+广义 SAM 与 SAM 的关系就是 AC 自动机与 KMP 自动机的关系。
+
+### 回文自动机
+
+[回文自动机](./pam.md) 比较特殊,它不能非常方便地定义为自动机。
+
+如果需要定义的话,它接受且仅接受某个字符串的所有回文子串的 **中心及右半部分**。
+
+“中心及右边部分”在奇回文串中就是字面意思,在偶回文串中定义为一个特殊字符加上右边部分。这个定义看起来很奇怪,但它能让 PAM 真正成为一个自动机,而不仅是两棵树。
+
+### 序列自动机
+
+[序列自动机](./seq-automaton.md) 接受且仅接受指定字符串的子序列。
+
+## 后缀链接
+
+由于自动机和匹配有着密不可分的关系,而匹配的一个基本思想是“这个串不行,就试试它的后缀可不可以”,所以在很多自动机(KMP、AC 自动机、SAM、PAM)中,都有后缀链接的概念。
+
+一个状态会对应若干字符串,而这个状态的后缀链接,是在自动机上的、是这些字符串的公共真后缀的字符串中,最长的那一个。
+
+需要注意的是,后缀链接在 DFA 上不一定是良定义的,但其在 KMP、AC 自动机、SAM 与 PAM 上是良定义的。
+
+一般来讲,后缀链接会形成一棵树,并且不同自动机的后缀链接树有着一些相同的性质,学习时可以加以注意。
+
+## 扩展阅读
+
+在计算复杂性领域中,自动机是一个经典的模型。并且,自动机与正则语言有着密不可分的关系。
+
+如果对相关内容感兴趣的话,推荐阅读博客 [计算复杂性(1) Warming Up: 自动机模型](https://lingeros-tot.github.io/2019/03/05/Warming-Up-自动机模型/)。
\ No newline at end of file
diff --git a/docs/string/general-sam.md b/docs/string/general-sam.md
new file mode 100644 (file)
index 0000000..e69de29
index 444e998..3799e59 100644 (file)
@@ -1,14 +1,17 @@
-在介绍 Hash 算法之前,首先你需要了解关于 [字符串匹配](/string/match) 的事情。
-
 ## Hash 的思想
 
-Hash 的核心思想在于,暴力算法中,单次比较的时间太长了,应当如何才能缩短一些呢?
-
-如果要求每次只能比较 $O(1)$ 个字符,应该怎样操作呢?
+Hash 的核心思想在于,将输入映射到一个值域较小、可以方便比较的范围。
 
-是比较第一个?最后一个?随便选一个?求所有字符的和?
+!!! warning
+    这里的“值域较小”在不同情况下意义不同。
+    
+    在 [哈希表](../ds/hash.md) 中,值域需要小到能够接受线性的空间与时间复杂度。
+    
+    在字符串哈希中,值域需要小到能够快速比较($10^9$、$10^{18}$ 都是可以快速比较的)。
+    
+    同时,为了降低哈希冲突率,值域也不能太小。
 
-我们定义一个把 string 映射成 int 的函数 $f$ ,这个 $f$ 称为是 Hash 函数。
+我们定义一个把字符串映射到整数的函数 $f$ ,这个 $f$ 称为是 Hash 函数。
 
 我们希望这个函数 $f$ 可以方便地帮我们判断两个字符串是否相等。
 
@@ -20,82 +23,76 @@ Hash 的核心思想在于,暴力算法中,单次比较的时间太长了,
 
 时间复杂度和 Hash 的准确率。
 
-通常我们采用的是多项式 Hash 的方法,即 $\operatorname{f}(s) = \sum s[i] \times b^i \pmod M$ 
+通常我们采用的是多项式 Hash 的方法,即 $f(s) = \sum s[i] \times b^i \pmod M$ 。
 
-这里面的 $b$ 和 $M$ 需要选取得足够合适才行,以使得 Hash 的冲突尽量均匀。
+这里面的 $b$ 和 $M$ 需要选取得足够合适才行,以使得 Hash 函数的值分布尽量均匀。
 
-如果 $b$ 和 $M$ 互质,在输入随机的情况下,这个 Hash 函数在 $[0,M)$ 上每个值概率相等
+如果 $b$ 和 $M$ 互质,在输入随机的情况下,这个 Hash 函数在 $[0,M)$ 上每个值概率相等,此时单次比较的错误率为 $\frac1M$ 。所以,哈希的模数一般会选用大质数。
 
-此时错误率为 $\frac1M$ (单次比较)
+## Hash 的实现
 
\9c¨è¾\93å\85¥ä¸\8dæ\98¯é\9a\8fæ\9cºç\9a\84æ\83\85å\86µä¸\8bï¼\8cæ\95\88æ\9e\9cä¹\9få¾\88好ã\80\82
\8f\82è\80\83代ç \81ï¼\9aï¼\88æ\95\88ç\8e\87ä½\8eä¸\8bç\9a\84ç\89\88æ\9c¬ï¼\8cå®\9eé\99\85使ç\94¨æ\97¶ä¸\80è\88¬ä¸\8dä¼\9aè¿\99ä¹\88å\86\99ï¼\89
 
-## Hash 的实现
+```cpp
+using std::string;
+
+const int M = 1e9 + 7;
+const int B = 233;
 
-伪代码:
+typedef long long ll;
 
-```text
-match_pre(int n) {
-    exp[0] = 1;
-    for (i = 1; i < n; i++) {
-        exp[i] = exp[i - 1] * b % M;
-    }
+int get_hash(const string& s)
+{
+    int res = 0;
+    for (int i = 0; i < s.size(); ++i)
+    {
+        res = (ll) (res * B + s[i]) % mod;
+       }
+    return res;
 }
 
-match(char *a, char *b, int n, int m) {
-    // match 函数返回:长度为 m 的串 b 在长度为 n 的串 a 中的匹配位置
-    // hash(a, m) 函数用来获得某个字符串前 m 个字符的部分的 hash 值
-    ans = new vector();
-    int ha = hash(a, m);
-    int hb = hash(b, m);
-    for (i = 0; i < n - m + 1; i++) {
-        if ((ha - hb * exp[i]) % M == 0) {
-            ans.push_back(i);
-        }
-        ha = (ha - a[i] * exp[i] + a[i + m] * exp[i + m]) % M;
-    }
-    return ans;
+void cmp(const string& s, const string& t)
+{
+    return get_hash(s) == get_hash(t);
 }
 ```
 
-通过上面这段代码,可以发现,每次直接计算 Hash 是 $O(串长)$ 的
-
 ## Hash 的分析与改进
 
-改进时间复杂度或者错误率
-
-在上述例子中,时间复杂度已经是 $O(n+m)$ 
-
-我们来分析错误率
+### 错误率
 
-由于 $n >> m$ ,要进行约 $n$ 次比较,每次错误率 $\frac1{M}$ ,那么总错误率是?
+由于 $n$ 远大于 $m$ ,要进行约 $n$ 次比较,每次错误率 $\frac1{M}$ ,那么总错误率是 $1-(1-1/M)^n$ 。在随机数据下,若 $M=10^9 + 7$, $n=10^6$,错误率约为 $\frac 1{1000}$,并不是能够完全忽略不计的。
 
-先补充一些随机数学的知识(非严格地)
+所以,进行字符串哈希时,经常会对两个大质数分别取模,这样的话哈希函数的值域就能扩大到两者之积,错误率就非常小了。
 
-现在考虑这 $n$ 次比较,如果看成独立的,总错误率 $1-(1-1/M)^n$ 
+### 多次询问子串哈希
 
½\93 $M >> n$ æ\97¶ï¼\8cæ\80»é\94\99误ç\8e\87æ\8e¥è¿\91äº\8e $\frac{n}{M}$ 
\8d\95次计ç®\97ä¸\80个å­\97符串ç\9a\84å\93\88å¸\8cå\80¼å¤\8dæ\9d\82度æ\98¯ $O(\text{串é\95¿})$ ç\9a\84ï¼\8cä¸\8eæ\9a´å\8a\9bå\8c¹é\85\8d没æ\9c\89å\8cºå\88«ï¼\8cå¦\82æ\9e\9cé\9c\80è¦\81å¤\9a次询é\97®ä¸\80个å­\97符串ç\9a\84å­\90串ç\9a\84å\93\88å¸\8cå\80¼ï¼\8cæ¯\8f次é\87\8dæ\96°è®¡ç®\97æ\95\88ç\8e\87é\9d\9e常ä½\8eä¸\8bã\80\82
 
-当 $M = n$ 时,接近于 $1-\frac{1}{e} (≈0.63)$ 
+一般采取的方法是对整个字符串先预处理出每个前缀的哈希值,将哈希值看成一个 $b$ 进制的数对 $M$ 取模的结果,这样的话每次就能快速求出子串的哈希了:
 
-如果不是独立的,最坏情况也就是全部加起来,等于 $\frac{n}{M}$ 
+令 $f_i(s)$ 表示 $f(s[1..i])$,那么 $f(s[l..r])=\frac{f_r[s]-f_{l-1}(s)}{b^{l-1}}$,其中 $\frac{1}{b^{l-1}}$ 也可以预处理出来,用 [乘法逆元](../math/inverse.md) 或者是在比较哈希值时等式两边同时乘上 $b$ 的若干次方化为整式均可。
 
¦\81æ\94¹è¿\9bé\94\99误ç\8e\87ï¼\8cå\8f¯ä»¥å¢\9eå\8a  $M$ 
¿\99æ ·ç\9a\84è¯\9dï¼\8cå°±å\8f¯ä»¥å\9c¨ $O(\text{串é\95¿})$ ç\9a\84é¢\84å¤\84ç\90\86å\90\8eæ¯\8f次 $O(1)$ å\9c°è®¡ç®\97å­\90串ç\9a\84å\93\88å¸\8cå\80¼äº\86ã\80\82
 
-选取一个大的 $M$ ,或者两个互质的小的 $M$ 
+## Hash 的应用
 
-时间复杂度不变,单次错误率平方
+### 字符串匹配
 
-## Hash 的应用
+求出模式串的哈希值后,求出文本串每个长度为模式串长度的子串的哈希值,分别与模式串的哈希值比较即可。
 
-不只是字符串中,在其他情况也可以用。
+### 最长回文子串
 
-假设有个程序要对 $10^{18}$ 大小的数组进行操作,保证只有 $10^6$ 个元素被访问到
+二分答案,判断是否可行时枚举回文中心(对称轴),哈希判断两侧是否相等。需要分别预处理正着和倒着的哈希值。时间复杂度 $O(n\log n)$。
 
-由于存不下,我们在每次操作时,对下标取 Hash 值(比如,直接 $\bmod M$ ),然后在 $M$ 大小的数组内操作
+这个问题可以使用 [manacher 算法](./manacher.md) 在 $O(n)$ 的时间内解决。
 
-如果冲突了怎么办?
+### 例题
 
-方案 1:尝试找下一个位置,下下个位置(优点:速度快;缺点:删除麻烦)
+???+note "[CF1200E Compress Words](http://codeforces.com/contest/1200/problem/E)"
+    题目大意:给你若干个字符串,答案串初始为空。第 $i$ 步将第 $i$ 个字符串加到答案串的后面,但是尽量地去掉重复部分(即去掉一个最长的、是原答案串的后缀、也是第 $i$ 个串的前缀的字符串),求最后得到的字符串。
+    
 
-方案 2:在数组的每个位置直接挂一个链表
+    每次需要求最长的、是原答案串的后缀、也是第 $i$ 个串的前缀的字符串。枚举这个串的长度,哈希比较即可。
+    
+    当然,这道题也可以使用 [KMP 算法](./kmp.md) 解决。
\ No newline at end of file
diff --git a/docs/string/images/automaton1.png b/docs/string/images/automaton1.png
new file mode 100644 (file)
index 0000000..52d247b
Binary files /dev/null and b/docs/string/images/automaton1.png differ
index f8bd23a..ca20b82 100644 (file)
@@ -1,28 +1,43 @@
-## å­\97符串æ\98¯å\95¥ï¼\9f
+## å®\9aä¹\89
 
-字符串可以看作是字符序列。
+### 字符集
 
-## 字符集
+一个**字符集** $Σ$ 是一个建立了全序关系的集合,也就是说,$Σ$ 中的任意两个不同的元素 $α$ 和 $β$ 都可以比较大小,要么 $α<β$,要么 $β<α$(也就是$α>β$)。字符集 $Σ$ 中的元素称为字符。
 
-字符集是符号和文字组成的集合,在 OI 中,处理字符串时计算复杂度往往要考虑到字符集大小带来的常数影响。
+### 字符串
 
-举个æ \97å­\90ï¼\8cå¦\82æ\9e\9cä¸\80é\81\93é¢\98å\8fªå\8c\85å\90«'A' ~ 'Z' æ\84\8få\91³ç\9d\80å­\97符é\9b\86大å°\8fæ\98¯ 26ã\80\82å¦\82æ\9e\9cå\86\8då\8a ä¸\8a '0' ï½\9e '9' å­\97符é\9b\86大å°\8få°±å\8f\98æ\88\90äº\86 36
+ä¸\80个**å­\97符串** $S$ æ\98¯å°\86 $n$ ä¸ªå­\97符顺次æ\8e\92å\88\97å½¢æ\88\90ç\9a\84åº\8få\88\97ï¼\8c$n$ ç§°ä¸º $S$ ç\9a\84é\95¿åº¦ï¼\8c表示为 $|S|$ã\80\82$S$ ç\9a\84第 $i$ ä¸ªå­\97符表示为 $S[i]$ã\80\82ï¼\88å\9c¨æ\9c\89ç\9a\84å\9c°æ\96¹ï¼\8cä¹\9fä¼\9aç\94¨ $S[i-1]$ è¡¨ç¤ºç¬¬ $i$ ä¸ªå­\97符ã\80\82ï¼\89
 
-计算复杂度时,字符集大小带来的常数往往要用 $\alpha$ 表示。
+### 子串
 
-## 如何存字符串
+字符串 $S$ 的**子串** $S[i..j],i≤j$,表示 $S$ 串中从 $i$ 到 $j$ 这一段,也就是顺次排列 $S[i],S[i+1],\ldots,S[j]$ 形成的字符串。
 
-可以开一个 `char` 数组 , 如 `char a[100]` 
+有时也会用 $S[i..j]$, $i>j$ 来表示空串。
 
-也可以用 `vector` 如 `vector<char> v` 
+### 子序列
 
\90\8cæ\97¶ STL ä¸­ä¹\9fæ\8f\90ä¾\9bäº\86å­\97符串容å\99¨ `std :: string` 
­\97符串 $S$ ç\9a\84**å­\90åº\8få\88\97**æ\98¯ä»\8e $S$ ä¸­å°\86è\8b¥å¹²å\85\83ç´ æ\8f\90å\8f\96å\87ºæ\9d¥å¹¶ä¸\8dæ\94¹å\8f\98ç\9b¸å¯¹ä½\8d置形æ\88\90ç\9a\84åº\8få\88\97ï¼\8cå\8d³ $S[p_1],S[p_2],\ldots,S[p_k]$ï¼\8c$1\le p_1<p_2<\cdots<p_k\le|S|$ã\80\82
 
-另外,在 `C/C++` 中也可以声明字符串字面量,比如 `char *buf = "XD"` 。
+### 后缀
 
-## 字符串存储的位置
+**后缀**是指从某个位置 $i$ 开始到整个串末尾结束的一个特殊子串。字符串 $S$ 的从 $i$ 开头的后缀表示为 $Suffix(S,i)$,也就是 $Suffix(S,i)=S[i..|S|]$。
 
--   字符串字面量:它们的值在编译过程中已经确定,保存在可执行目标文件的 `.rodata` 段内。
-    调用 `objdump -s -j .rodata 文件名` 可以查看 `.rodata` 段的具体内容。
--   字符数组:局部变量保存在栈中,全局变量若初始化为非 0 值则保存在可执行目标文件的 `.data` 段内,若未初始化或初始化为 0 则保存在 `.bss` 段。
--    `string` 、 `vector<char>` : 它们的字符元素一般存储在堆区,由 stl 调用 `malloc` 或者 `new` 开辟存储元素的空间。
+**真后缀** 指除了 $S$ 本身的 $S$ 的后缀。
+
+### 字典序
+
+以第 $i$ 个字符作为第 $i$ 关键字进行大小比较,空字符小于字符集内任何字符(即:$a<aa$)。
+
+### 回文串
+
+**回文串** 是正着写和倒着写相同的字符串,即满足 $\forall 1\le i\le|s|, s[i]=s[|s|+1-i]$ 的 $s$。
+
+## 字符串的存储
+
+1. 使用 `char` 数组存储,用空字符 `\0` 表示字符串的结尾。(C 风格字符串)
+2. 使用 C++ 标准库提供的 [`string` 类](../lang/csl/string.md)。
+3. 字符串常量可以用字符串字面值(用双引号括起来的字符串)表示。
+
+## 参考资料
+
+[后缀数组 by.徐智磊](https://wenku.baidu.com/view/0dc03d2b1611cc7931b765ce0508763230127479.html)
\ No newline at end of file
index 8d0e538..289c334 100644 (file)
@@ -1,16 +1,14 @@
 ## 字符串匹配问题
 
-字符串匹配问题分为好多类:
-
 ### 单串匹配
 
 一个模式串 (pattern),一个待匹配串,找出前者在后者中的所有出现位置
 
 ### 多串匹配
 
-多个模式串,一个待匹配串(多个待匹配串还用说,直接连起来)
+多个模式串,一个待匹配串(多个待匹配串可以直接连起来)。
 
-直接当做单串匹配肯定是可以的,但是效率不够高
+直接当做单串匹配肯定是可以的,但是效率不够高
 
 ### 匹配一个串的任意后缀
 
 
 ## 暴力做法
 
-对于每个位置,尝试对模式串和待匹配串进行比对
+对于每个位置,尝试对模式串和待匹配串进行比对。
+
+参考代码:
 
 (伪代码)
 
-```text
-match(char *a, char *b, int n, int m) {
-       ans = new vector();
+```cpp
+std::vector<int> match(char *a, char *b, int n, int m) {
+       std::vector<int> ans;
        for (i = 0; i < n - m + 1; i++) {
                for (j = 0; j < m; j++) {
                        if (a[i + j] != b[j]) break;
@@ -41,12 +41,12 @@ match(char *a, char *b, int n, int m) {
 
 最好是 $O(n)$ 的。
 
-如果字符集的大小大于 1(有至少两个不同的字符),平均时间复杂度是 $O(n)$ 的。
+如果字符集的大小大于 1(有至少两个不同的字符),平均时间复杂度是 $O(n)$ 的。但是在 OI 题目中,给出的字符串一般都不是纯随机的。
 
 ## Hash 的方法
 
-参见 [Hash](/string/hash
+参见 [Hash](./hash.md
 
 ## KMP 算法
 
-参见 [KMP](/string/kmp/#knuth-morris-pratt
+参见 [KMP](./kmp.md
index f77d35c..699f76b 100644 (file)
@@ -150,7 +150,7 @@ nav:
     - 字符串部分简介: string/index.md
     - 标准库: string/lib-func.md
     - 字符串匹配: string/match.md
-    - 哈希: string/hash.md
+    - å­\97符串å\93\88å¸\8c: string/hash.md
     - 字典树 (Trie): string/trie.md
     - 前缀函数与 KMP 算法: string/kmp.md
     - Z 函数(扩展 KMP): string/z-func.md
@@ -158,10 +158,11 @@ nav:
     - AC 自动机: string/ac-automaton.md
     - 后缀数组 (SA): string/sa.md
     - 后缀自动机 (SAM): string/sam.md
+    - 广义后缀自动机: string/general-sam.md
     - 后缀树: string/suffix-tree.md
     - Manacher: string/manacher.md
     - 回文自动机: string/pam.md
-    - 序列自动机: string/sequence-am.md
+    - 序列自动机: string/seq-automaton.md
     - 最小表示法: string/minimal-string.md
     - Lyndon 分解: string/lyndon.md
   - 数学: