OSDN Git Service

Update sam.md
authorTrisolaris HD <36555123+TrisolarisHD@users.noreply.github.com>
Fri, 29 Mar 2019 11:10:28 +0000 (19:10 +0800)
committerGitHub <noreply@github.com>
Fri, 29 Mar 2019 11:10:28 +0000 (19:10 +0800)
docs/string/sam.md

index 267a0b3..d3efeee 100644 (file)
@@ -148,7 +148,7 @@ $$
 
 为了保证线性的空间复杂度,我们将只保存 $len$ 和 $link$ 的值和每个状态的转移列表,我们不会标记终止状态(但是我们稍后会展示在构造 SAM 后如何分配这些标记)。
 
-一开始 SAM 只包含一个状态 $t_0$ ,编号为 $0$ (其它状态的编号为 $1,\,2,\,\ldots$ )。为了方便,对于状态 $t_0$ 我们指定 $len=0$ 、 $link=-1$ ( $-1$ 表示虚拟状态)。
+一开始 SAM 只包含一个状态 $t_0$ ,编号为 $0$ (其它状态的编号为 $1,2,\ldots$ )。为了方便,对于状态 $t_0$ 我们指定 $len=0$ 、 $link=-1$ ( $-1$ 表示虚拟状态)。
 
 现在,任务转化为实现给当前字符串添加一个字符 $c$ 的过程。算法流程如下:
 
@@ -166,7 +166,7 @@ $$
 
 如果我们还想知道哪些状态是 **终止状态** 而哪些不是,我们可以在为字符串 $s$ 构造完完整的 SAM 后找到所有的终止状态。为此,我们从对应整个字符串的状态(存储在变量 $last$ 中),遍历它的后缀链接,直到到达初始状态。我们将所有遍历到的节点都标记为终止节点。容易理解这样做我们会准确地标记字符串 $s$ 的所有后缀,这些状态都是终止状态。
 
-在下一部分,我们将检查算法每一步的细节,并证明它的 **正确性** 。
+在下一部分,我们将详细叙述算法每一步的细节,并证明它的 **正确性** 。
 
 因为我们只为 $s$ 的每个字符创建一个或两个新状态,所以 SAM 只包含 **线性个** 状态。
 
@@ -188,7 +188,7 @@ $$
 
 ### 对操作次数为线性的证明
 
-首先我们假设字符集大小为 **常数** 。如果字符集大小不是常数,SAM 的时间复杂度就不是线性的。从一个结点出发的转移存储在支持快速查询和插入的平衡树中。因此如果我们记 $k$ 为字符集的大小,则算法的渐进时间复杂度为 $O(n\log k)$ ,空间复杂度为 $O(n)$ 。然而如果字符集足够小,可以不写平衡树,以空间换时间将每个结点的转移存储为长度为 $k$ 的数组(用于快速查询)和链表(用于快速遍历所有可用关键字)。这样算法的时间复杂度为 $O(n)$ ,空间复杂度为 $O(nk)$ 。
+首先我们假设字符集大小为 **常数** 。如果字符集大小不是常数,SAM 的时间复杂度就不是线性的。从一个结点出发的转移存储在支持快速查询和插入的平衡树中。因此如果我们记 $\Sigma$ 为字符集,$\left|\Sigma\right|$ 为字符集大小,则算法的渐进时间复杂度为 $O(n\log\left|\Sigma\right|)$ ,空间复杂度为 $O(n)$ 。然而如果字符集足够小,可以不写平衡树,以空间换时间将每个结点的转移存储为长度为 $\left|\Sigma\right|$ 的数组(用于快速查询)和链表(用于快速遍历所有可用关键字)。这样算法的时间复杂度为 $O(n)$ ,空间复杂度为 $O(n\left|\Sigma\right|)$ 。
 
 所以我们将认为字符集的大小为常数,即每次对一个字符搜索转移、添加转移、查找下一个转移。这些操作的时间复杂度都为 $O(1)$ 。
 
@@ -208,12 +208,12 @@ $$
 
 ### 实现
 
-首先,我们实现一种存储一个转移的全部信息的数据结构。如果需要的话,你可以在这里加入一个终止标记,也可以是一些其它信息。我们将用一个 `map` 存储转移的列表,允许我们在总计 $O(n)$ 的空间复杂度和 $O(n\log k)$ 的时间复杂度内处理整个字符串。
+首先,我们实现一种存储一个转移的全部信息的数据结构。如果需要的话,你可以在这里加入一个终止标记,也可以是一些其它信息。我们将用一个 `map` 存储转移的列表,允许我们在总计 $O(n)$ 的空间复杂度和 $O(n\log\left|\Sigma\right|)$ 的时间复杂度内处理整个字符串。
 
 ```cpp
 struct state {
   int len, link;
-  map<char, int> next;
+  std::map<char, int> next;
 };
 ```
 
@@ -225,7 +225,7 @@ state st[MAXLEN * 2];
 int sz, last;
 ```
 
-我们定义一个函数来初始化 SAM (创建一个只有一个状态的 SAM)。
+我们定义一个函数来初始化 SAM (创建一个只有初始状态的 SAM)。
 
 ```cpp
 void sa_init() {
@@ -236,7 +236,7 @@ void sa_init() {
 }
 ```
 
-最终我们给出主函数的实现:给当前行末增加一个字符,对应地重建自动机。
+最终我们给出主函数的实现:给当前行末增加一个字符,对应地在之前的基础上建造自动机。
 
 ```cpp
 void sa_extend(char c) {
@@ -269,13 +269,13 @@ void sa_extend(char c) {
 }
 ```
 
-正如之前提到的一样,如果你用内存换时间(空间复杂度为 $O(nk)$ ,其中 $k$ 为字符集大小),你可以在 $O(n)$ 的时间内构造字符集大小 $k$ 任意的 SAM。但是这样你需要为每一个状态储存一个大小为 $k$ 的数组(用于快速跳转到转移的字符),和另外一个所有转移的链表(用于快速在转移中迭代)。
+正如之前提到的一样,如果你用内存换时间(空间复杂度为 $O(n\left|\Sigma\right|)$ ,其中 $\left|\Sigma\right|$ 为字符集大小),你可以在 $O(n)$ 的时间内构造字符集大小任意的 SAM。但是这样你需要为每一个状态储存一个大小为 $\left|\Sigma\right|$ 的数组(用于快速跳转到转移的字符),和另外一个所有转移的链表(用于快速在转移中迭代)。
 
 ## 更多性质
 
 ### 状态数
 
-对于一个长度为 $n$ 的字符串 $s$ ,它的 SAM 中的状态数 **不会超过** $2n-1$ (假设 $n\ge2$ )。
+对于一个长度为 $n$ 的字符串 $s$ ,它的 SAM 中的状态数 **不会超过** $2n-1$ (假设 $n\ge 2$ )。
 
 算法本身即可证明该结论。一开始,自动机含有一个状态,第一次和第二次迭代中只会创建一个节点,剩余的 $n-2$ 步中每步会创建至多 $2$ 个状态。
 
@@ -318,7 +318,7 @@ void sa_extend(char c) {
 考虑到 SAM 为有向无环图,不同路径的条数可以通过动态规划计算。即令 $d[v]$ 为从状态 $v$ 开始的路径数量(包括长度为零的路径),则我们有如下递推方程:
 
 $$
-d[v]=1+\sum_{w:(v,\,w,\,c)\in DAWG}d[w]
+d[v]=1+\sum_{w:(v,w,c)\in DAWG}d[w]
 $$
 
 即, $d[v]$ 可以表示为所有 $v$ 的转移的末端的和。
@@ -336,7 +336,7 @@ $$
 我们已经在上一题中介绍了如何计算 $d[v]$ 。 $ans[v]$ 的值可以通过以下递推式计算:
 
 $$
-ans[v]=\sum_{w:(v,\,w,\,c)\in DAWG}d[w]+ans[w]
+ans[v]=\sum_{w:(v,w,c)\in DAWG}d[w]+ans[w]
 $$
 
 我们取每个邻接结点 $w$ 的答案,并加上 $d[w]$ (因为从状态 $v$ 出发的子串都增加了一个字符)。
@@ -345,19 +345,19 @@ $$
 
 ### 字典序第 $k$ 大子串
 
-> 给定一个字符串 $S$ 。多组询问,每组询问给定一个数 $K_i$ ,查询所有子串中字典序第 $K_i$ 大的子串。
+> 给定一个字符串 $S$ 。多组询问,每组询问给定一个数 $K_i$ ,查询 $S$ 所有子串中字典序第 $K_i$ 大的子串。
 
-解å\86³è¿\99个é\97®é¢\98ç\9a\84æ\80\9dè·¯å\9fºäº\8eå\89\8d两个é\97®é¢\98ç\9a\84æ\80\9dè·¯ã\80\82å­\97å\85¸åº\8f第 $k$ å¤§ç\9a\84å­\90串对åº\94äº\8e SAM ä¸­å­\97å\85¸åº\8f第 $k$ å¤§ç\9a\84è·¯å¾\84ã\80\82因此在计算每个状态的路径数后,我们可以很容易地从 SAM 的根开始找到第 $k$ 大的路径。
+解å\86³è¿\99个é\97®é¢\98ç\9a\84æ\80\9dè·¯å\8f¯ä»¥ä»\8e解å\86³å\89\8d两个é\97®é¢\98ç\9a\84æ\80\9dè·¯å\8f\91å±\95è\80\8cæ\9d¥ã\80\82å­\97å\85¸åº\8f第 $k$ å¤§ç\9a\84å­\90串对åº\94äº\8e SAM ä¸­å­\97å\85¸åº\8f第 $k$ å¤§ç\9a\84è·¯å¾\84ï¼\8c因此在计算每个状态的路径数后,我们可以很容易地从 SAM 的根开始找到第 $k$ 大的路径。
 
-预处理的时间复杂度为 $O(length(S))$ ,单次查询的复杂度为 $O(length(ans)\cdot k)$ (其中 $ans$ 是查询的答案, $k$ 为字符集的大小)。
+预处理的时间复杂度为 $O(length(S))$ ,单次查询的复杂度为 $O(length(ans)\cdot\left|\Sigma\right|)$ (其中 $ans$ 是查询的答案, $\left|\Sigma\right|$ 为字符集的大小)。
 
 ### 最小循环移位
 
 > 给定一个字符串 $S$ 。找出字典序最小的循环移位。
 
-我们为字符串 $S+S$ 构造 SAM 。则 SAM 本身将包含字符串 $S$ 的所有循环移位作为路径
+容易发现字符串 $S+S$ 包含字符串 $S$ 的所有循环移位作为子串
 
-所以问题简化为寻找最小的长度为 $length(S)$ 的路径,这可以通过平凡的方法做到:我们从初始状态开始,贪心地访问最小的字符即可。
\89\80以é\97®é¢\98ç®\80å\8c\96为å\9c¨ $S+S$ å¯¹åº\94ç\9a\84å\90\8eç¼\80è\87ªå\8a¨æ\9cºä¸\8a寻æ\89¾æ\9c\80å°\8fç\9a\84é\95¿åº¦ä¸º $length(S)$ ç\9a\84è·¯å¾\84ï¼\8cè¿\99å\8f¯ä»¥é\80\9aè¿\87å¹³å\87¡ç\9a\84æ\96¹æ³\95å\81\9aå\88°ï¼\9aæ\88\91们ä»\8eå\88\9då§\8bç\8a¶æ\80\81å¼\80å§\8bï¼\8cè´ªå¿\83å\9c°è®¿é\97®æ\9c\80å°\8fç\9a\84å­\97符å\8d³å\8f¯ã\80\82
 
 总的时间复杂度为 $O(length(S))$ 。