OSDN Git Service

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

index d3efeee..72c6c1b 100644 (file)
@@ -1,3 +1,10 @@
+## 一些前置约定/定义
+
+记 $\Sigma$ 为字符集,$\left|\Sigma\right|$ 为字符集大小。
+对于一个字符串 $s$,记 $\left|s\right|$ 为其长度。
+
+## 后缀自动机概述
+
  **后缀自动机** (suffix automaton, SAM) 是一个能解决许多字符串相关问题的有力的数据结构。
 
 举个例子,以下的字符串问题都可以在线性时间内通过 SAM 解决。
@@ -5,7 +12,7 @@
 -   在另一个字符串中搜索一个字符串的所有出现位置。
 -   计算给定的字符串中有多少个不同的子串。
 
-直观上,字符串的 SAM 可以理解为给定字符串的 **所有子串** 的压缩形式。值得注意的事实是, SAM 将所有的这些信息以高度压缩的形式储存。对于一个长度为 $n$ 的字符串,它的空间复杂度仅为 $O(n)$ 。此外,构造 SAM 的时间复杂度仅为 $O(n)$ (这里我们将字符集的大小 $k$ 看作常数,否则时间复杂度和空间复杂度均为 $O(n\log k)$ )。
+直观上,字符串的 SAM 可以理解为给定字符串的 **所有子串** 的压缩形式。值得注意的事实是, SAM 将所有的这些信息以高度压缩的形式储存。对于一个长度为 $n$ 的字符串,它的空间复杂度仅为 $O(n)$ 。此外,构造 SAM 的时间复杂度仅为 $O(n)$ (这里我们将字符集的大小 $\left||\Sigma\right|$ 看作常数,否则时间复杂度和空间复杂度均为 $O(n\log\left|\Sigma\right|)$ )。
 
 ## SAM 的定义
 
@@ -73,11 +80,11 @@ SAM 最简单、也最重要的性质是,它包含关于字符串 $s$ 的所
 
 由 $endpos$ 的值我们可以得到一些重要结论:
 
->  **引理 1:** 两个非空子串 $u$ 和 $w$ (假设 $length(u)\le length(w)$ )的 $endpos$ 相同,当且仅当字符串 $u$ 是 $w$ 的后缀。
+>  **引理 1:** 两个非空子串 $u$ 和 $w$ (假设 $\left|u\right|\le \left|w\right|$ )的 $endpos$ 相同,当且仅当字符串 $u$ 是 $w$ 的后缀。
 
 引理显然成立。如果 $u$ 和 $w$ 的 $endpos$ 相同,则 $u$ 是 $w$ 的一个后缀,且只以 $s$ 中的一个 $w$ 的后缀的形式出现。且根据定义,如果 $u$ 为 $w$ 的一个后缀,且只以后缀的形式在 $s$ 中出现时,两个子串的 $endpos$ 相同。
 
->  **引理 2:** 考虑两个非空子串 $u$ 和 $w$ (假设 $length(u)\le length(w)$ )。那么要么 $endpos(u)\cap endpos(w)=\varnothing$ ,要么 $endpos(w)\subseteq endpos(u)$ ,取决于 $u$ 是否为 $w$ 的一个后缀:
+>  **引理 2:** 考虑两个非空子串 $u$ 和 $w$ (假设 $\left|u\right|\le \left|w\right|$ )。那么要么 $endpos(u)\cap endpos(w)=\varnothing$ ,要么 $endpos(w)\subseteq endpos(u)$ ,取决于 $u$ 是否为 $w$ 的一个后缀:
 >
 > $$
 > \begin{cases}
@@ -88,13 +95,13 @@ SAM 最简单、也最重要的性质是,它包含关于字符串 $s$ 的所
 
 证明:如果集合 $endpos(u)$ 与 $endpos(w)$ 有至少一个公共元素,那么由于字符串 $u$ 与 $w$ 在相同位置结束, $u$ 是 $w$ 的一个后缀。所以在每次 $w$ 出现的位置,子串 $u$ 也会出现。所以 $endpos(w)\subseteq endpos(u)$ 。
 
->  **引理 3:** 考虑一个 $endpos$ 等价类,将类中的所有子串按长度非递增的顺序排序。每个子串都不会比它前一个子串长,与此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中的子串长度恰好覆盖整个区间 $[x,\,y]$ 。
+>  **引理 3:** 考虑一个 $endpos$ 等价类,将类中的所有子串按长度非递增的顺序排序。每个子串都不会比它前一个子串长,与此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中的子串长度恰好覆盖整个区间 $[x,y]$ 。
 
 证明:如果 $endpos$ 等价类中只包含一个子串,引理显然成立。现在我们来讨论子串元素个数大于 $1$ 的等价类。
 
 由引理 1,两个不同的 $endpos$ 等价的字符串中,较短者总是较长者的真后缀。因此,等价类中没有等长的字符串。
 
-记 $w$ 为等价类中最长的字符串、 $u$ 为等价类中最短的字符串。由引理 1,字符串 $u$ 是字符串 $w$ 的真后缀。现在考虑长度在区间 $[length(u),\,length(w)]$ 中的 $w$ 的任意后缀。容易看出,这个后缀也在同一等价类中,因为这个后缀只能在字符串 $s$ 中以 $w$ 的一个后缀的形式存在(也因为较短的后缀 $u$ 在 $s$ 中只以 $w$ 的后缀的形式存在)。因此,由引理 1,这个后缀和字符串 $w$ 的 $endpos$ 相同。
+记 $w$ 为等价类中最长的字符串、 $u$ 为等价类中最短的字符串。由引理 1,字符串 $u$ 是字符串 $w$ 的真后缀。现在考虑长度在区间 $[\left|u\right|,\left|w\right|]$ 中的 $w$ 的任意后缀。容易看出,这个后缀也在同一等价类中,因为这个后缀只能在字符串 $s$ 中以 $w$ 的一个后缀的形式存在(也因为较短的后缀 $u$ 在 $s$ 中只以 $w$ 的后缀的形式存在)。因此,由引理 1,这个后缀和字符串 $w$ 的 $endpos$ 相同。
 
 ### 后缀链接 $link$
 
@@ -104,7 +111,7 @@ SAM 最简单、也最重要的性质是,它包含关于字符串 $s$ 的所
 
 换句话说,一个 **后缀链接** $link(v)$ 连接到对应于 $w$ 的最长后缀的另一个 $endpos$ 等价类的状态。
 
-以下我们假设初始状态 $t_0$ 对应于它自己这个等价类(只包含一个空字符串)。为了方便,我们规定 $endpos(t)=\{-1,\,0,\,\ldots,\,length(s)-1\}$ 。
+以下我们假设初始状态 $t_0$ 对应于它自己这个等价类(只包含一个空字符串)。为了方便,我们规定 $endpos(t)=\{-1,0,\ldots,\left|S\right|-1\}$ 。
 
 >  **引理 4:** 所有后缀链接构成一棵根节点为 $t_0$ 的树。
 
@@ -112,7 +119,7 @@ SAM 最简单、也最重要的性质是,它包含关于字符串 $s$ 的所
 
 >  **引理 5:** 通过 $endpos$ 集合构造的树(每个子节点的 $subset$ 都包含在父节点的 $subset$ 中)与通过后缀链接 $link$ 构造的树相同。
 
-证明:由引理 2,我们可以用 $endpos$ 集合构造一棵树(因为两个集合要么完全没有交集要么其中一个是另一个的子集)。
+证明:由引理 2,任意一个 SAM 的 $endpos$ 集合形成了一棵树(因为两个集合要么完全没有交集要么其中一个是另一个的子集)。
 
 我们现在考虑任意不是 $t_0$ 的状态 $v$ 及后缀链接 $link(v)$ ,由后缀链接和引理 2,我们可以得到
 
@@ -132,7 +139,7 @@ $$
 
 -    $s$ 的子串可以根据它们结束的位置 $endpos$ 被划分为多个等价类;
 -    SAM 由初始状态 $t_0$ 和与每一个 $endpos$ 等价类对应的每个状态组成;
--   对于每一个状态 $v$ ,一个或多个子串与之匹配。我们记 $longest(v)$ 为其中最长的一个字符串,记 $len(v)$ 为它的长度。类似地,记 $shortest(v)$ 为最短的子串,它的长度为 $minlen(v)$ 。那么对应这个状态的所有字符串都是字符串 $longest(v)$ 的不同的后缀,且所有字符串的长度恰好覆盖区间 $[minlength(v),\,len(v)]$ 中的每一个整数。
+-   对于每一个状态 $v$ ,一个或多个子串与之匹配。我们记 $longest(v)$ 为其中最长的一个字符串,记 $len(v)$ 为它的长度。类似地,记 $shortest(v)$ 为最短的子串,它的长度为 $minlen(v)$ 。那么对应这个状态的所有字符串都是字符串 $longest(v)$ 的不同的后缀,且所有字符串的长度恰好覆盖区间 $[minlength(v),len(v)]$ 中的每一个整数。
 -   对于任意不是 $t_0$ 的状态 $v$ ,定义后缀链接为连接到对应字符串 $longest(u)$ 的长度为 $minlen(v)-1$ 的后缀的一条边。从根节点 $t_0$ 出发的后缀链接可以形成一棵树。这棵树也表示 $endpos$ 集合间的包含关系。
 -   对于 $t_0$ 以外的状态 $v$ ,可用后缀链接 $link(v)$ 表达 $minlen(v)$ :
 
@@ -140,7 +147,7 @@ $$
 minlen(v)=len(link(v))+1.
 $$
 
--   如果我们从任意状态 $v_0$ 开始顺着后缀链接遍历,总会到达初始状态 $t_0$ 。这种情况下我们可以得到一个互不相交的区间 $[minlen(v_i),\,len(v_i)]$ 的序列,且它们的并集形成了连续的区间 $[0,\,len(v_0)]$ 。
+-   如果我们从任意状态 $v_0$ 开始顺着后缀链接遍历,总会到达初始状态 $t_0$ 。这种情况下我们可以得到一个互不相交的区间 $[minlen(v_i),len(v_i)]$ 的序列,且它们的并集形成了连续的区间 $[0,len(v_0)]$ 。
 
 ### 算法
 
@@ -174,12 +181,12 @@ $$
 
 ### 正确性证明
 
--   若一个转移 $(p,\,q)$ 满足 $len(p)+1=len(q)$ ,则我们称这个转移是 **连续的** 。否则,即当 $len(p)+1<len(q)$ 时,这个转移被称为 **不连续的** 。从算法描述中可以看出,连续的、不连续的转移是算法的不同情况。连续的转移是固定的,我们不会再改变了。与此相反,当向字符串中插入一个新的字符时,不连续的转移可能会改变(转移边的端点可能会改变)。
+-   若一个转移 $(p,q)$ 满足 $len(p)+1=len(q)$ ,则我们称这个转移是 **连续的** 。否则,即当 $len(p)+1<len(q)$ 时,这个转移被称为 **不连续的** 。从算法描述中可以看出,连续的、不连续的转移是算法的不同情况。连续的转移是固定的,我们不会再改变了。与此相反,当向字符串中插入一个新的字符时,不连续的转移可能会改变(转移边的端点可能会改变)。
 -   为了避免引起歧义,我们记向 SAM 中插入当前字符 $c$ 之前的字符串为 $s$ 。
 -   算法从创建一个新状态 $cur$ 开始,对应于整个字符串 $s+c$ 。我们创建一个新的节点的原因很清楚。与此同时我们也创建了一个新的字符和一个新的等价类。
 -   在创建一个新的状态之后,我们会从对应整个字符串 $s$ 的状态通过后缀链接进行遍历。对于每一个状态,我们尝试添加一个通过字符 $c$ 到新状态 $cur$ 的转移。然而我们只能添加与原有转移不冲突的转移。因此我们只要找到已存在的 $c$ 的转移,我们就必须停止。
 -   最简单的情况是我们到达了虚拟状态 $-1$ ,这意味着我们为所有 $s$ 的后缀添加了 $c$ 的转移。这也意味着,字符 $c$ 从未在字符串 $s$ 中出现过。因此 $cur$ 的后缀链接为状态 $0$ 。
--   第二种情况下,我们找到了现有的转移 $(p,\,q)$ 。这意味着我们尝试向自动机内添加一个 **已经存在的** 字符串 $x+c$ (其中 $x$ 为 $s$ 的一个后缀,且字符串 $x+c$ 已经作为 $s$ 的一个子串出现过了)。因为我们假设字符串 $s$ 的自动机的构造是正确的,我们不应该在这里添加一个新的转移。然而,难点在于,从状态 $cur$ 出发的后缀链接应该连接到哪个状态呢?我们要把后缀链接连到一个状态上,且其中最长的一个字符串恰好是 $x+c$ ,即这个状态的 $len$ 应该是 $len(p)+1$ 。然而还不存在这样的状态,即 $len(q)>len(p)+1$ 。这种情况下,我们必须通过拆开状态 $q$ 来创建一个这样的状态。
+-   第二种情况下,我们找到了现有的转移 $(p,q)$ 。这意味着我们尝试向自动机内添加一个 **已经存在的** 字符串 $x+c$ (其中 $x$ 为 $s$ 的一个后缀,且字符串 $x+c$ 已经作为 $s$ 的一个子串出现过了)。因为我们假设字符串 $s$ 的自动机的构造是正确的,我们不应该在这里添加一个新的转移。然而,难点在于,从状态 $cur$ 出发的后缀链接应该连接到哪个状态呢?我们要把后缀链接连到一个状态上,且其中最长的一个字符串恰好是 $x+c$ ,即这个状态的 $len$ 应该是 $len(p)+1$ 。然而还不存在这样的状态,即 $len(q)>len(p)+1$ 。这种情况下,我们必须通过拆开状态 $q$ 来创建一个这样的状态。
 -   如果转移 $(p,\,q)$ 是连续的,那么 $len(q)=len(p)+1$ 。在这种情况下一切都很简单。我们只需要将 $cur$ 的后缀链接指向状态 $q$ 。
 -   否则转移是不连续的,即 $len(q)>len(p)+1$ ,这意味着状态 $q$ 不只对应于长度为 $len(p)+1$ 的后缀 $s+c$ ,还对应于 $s$ 的更长的子串。除了将状态 $q$ 拆成两个子状态以外我们别无他法,所以第一个子状态的长度就是 $len(p)+1$ 了。  
     我们如何拆开一个状态呢?我们 **复制** 状态 $q$ ,产生一个状态 $clone$ ,我们将 $len(clone)$ 赋值为 $len(p)+1$ 。由于我们不想改变遍历到 $q$ 的路径,我们将 $q$ 的所有转移复制到 $clone$ 。我们也将从 $clone$ 出发的后缀链接设置为 $q$ 的后缀链接的目标,并设置 $q$ 的后缀链接为 $clone$ 。  
@@ -303,53 +310,53 @@ void sa_extend(char c) {
 
 > 给一个文本串 $T$ 和多个模式串 $P$ ,我们要检查字符串 $P$ 是否作为 $T$ 的一个子串出现。
 
-我们在 $O(length(T))$ 的时间内为文本串 $T$ 构造 SAM。为了检查模式串 $P$ 是否在 $T$ 中出现,我们沿转移(边)从 $t_0$ 开始根据 $P$ 的字符进行转移。如果在某个点无法转移下去,则模式串 $P$ 不是 $T$ 的一个子串。如果我们能够这样处理完整个字符串 $P$ ,那么模式串在 $T$ 中出现过。
+我们在 $O(\left|T\right|)$ 的时间内对文本串 $T$ 构造后缀自动机。为了检查模式串 $P$ 是否在 $T$ 中出现,我们沿转移(边)从 $t_0$ 开始根据 $P$ 的字符进行转移。如果在某个点无法转移下去,则模式串 $P$ 不是 $T$ 的一个子串。如果我们能够这样处理完整个字符串 $P$ ,那么模式串在 $T$ 中出现过。
 
-对于每个字符串 $P$ ,算法的时间复杂度为 $O(length(P))$ 。此外,这个算法还找到了模式串 $P$ 在文本串中出现的最大前缀长度。
+对于每个字符串 $P$ ,算法的时间复杂度为 $O(\left|P\right|)$ 。此外,这个算法还找到了模式串 $P$ 在文本串中出现的最大前缀长度。
 
 ### 不同子串个数
 
 > 给一个字符串 $S$ ,计算不同子串的个数。
 
-为字符串 $S$ 构造 SAM 
+对字符串 $S$ 构造后缀自动机
 
 每个 $S$ 的子串都相当于自动机中的一些路径。因此不同子串的个数等于自动机中以 $t_0$ 为起点的不同路径的条数。
 
-考虑到 SAM 为有向无环图,不同路径的条数可以通过动态规划计算。即令 $d[v]$ 为从状态 $v$ 开始的路径数量(包括长度为零的路径),则我们有如下递推方程:
+考虑到 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$ 的转移的末端的和。
+即, $d_{v}$ 可以表示为所有 $v$ 的转移的末端的和。
 
-所以不同子串的个数为 $d[t_0]-1$ (因为要去掉空子串)。
+所以不同子串的个数为 $d_{t_0}-1$ (因为要去掉空子串)。
 
-总时间复杂度为: $O(length(S))$ 。
+总时间复杂度为: $O(\left|S\right|)$ 。
 
 ### 所有不同子串的总长度
 
 > 给定一个字符串 $S$ ,计算所有不同子串的总长度。
 
-本题做法与上一题类似,只是现在我们需要考虑分两部分进行动态规划:不同子串的数量 $d[v]$ 和它们的总长度 $ans[v]$ 。
+本题做法与上一题类似,只是现在我们需要考虑分两部分进行动态规划:不同子串的数量 $d_{v}$ 和它们的总长度 $ans_{v}$ 。
 
-我们已经在上一题中介绍了如何计算 $d[v]$ 。 $ans[v]$ 的值可以通过以下递推式计算:
+我们已经在上一题中介绍了如何计算 $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$ 出发的子串都增加了一个字符)。
+我们取每个邻接结点 $w$ 的答案,并加上 $d_{w}$ (因为从状态 $v$ 出发的子串都增加了一个字符)。
 
-算法的时间复杂度仍然是 $O(length(S))$ 。
+算法的时间复杂度仍然是 $O(\left|S\right|)$ 。
 
 ### 字典序第 $k$ 大子串
 
-> 给定一个字符串 $S$ 。多组询问,每组询问给定一个数 $K_i$ ,查询 $S$ 所有子串中字典序第 $K_i$ 大的子串。
+> 给定一个字符串 $S$ 。多组询问,每组询问给定一个数 $K_i$ ,查询 $S$ 所有子串中字典序第 $K_i$ 大的子串。
 
 解决这个问题的思路可以从解决前两个问题的思路发展而来。字典序第 $k$ 大的子串对应于 SAM 中字典序第 $k$ 大的路径,因此在计算每个状态的路径数后,我们可以很容易地从 SAM 的根开始找到第 $k$ 大的路径。
 
-预处理的时间复杂度为 $O(length(S))$ ,单次查询的复杂度为 $O(length(ans)\cdot\left|\Sigma\right|)$ (其中 $ans$ 是查询的答案, $\left|\Sigma\right|$ 为字符集的大小)。
+预处理的时间复杂度为 $O(\left|S\right|)$ ,单次查询的复杂度为 $O(\left|ans\right|\cdot\left|\Sigma\right|)$ (其中 $ans$ 是查询的答案, $\left|\Sigma\right|$ 为字符集的大小)。
 
 ### 最小循环移位
 
@@ -357,43 +364,43 @@ $$
 
 容易发现字符串 $S+S$ 包含字符串 $S$ 的所有循环移位作为子串。
 
-所以问题简化为在 $S+S$ 对应的后缀自动机上寻找最小的长度为 $length(S)$ 的路径,这可以通过平凡的方法做到:我们从初始状态开始,贪心地访问最小的字符即可。
+所以问题简化为在 $S+S$ 对应的后缀自动机上寻找最小的长度为 $\left|S\right|$ 的路径,这可以通过平凡的方法做到:我们从初始状态开始,贪心地访问最小的字符即可。
 
-总的时间复杂度为 $O(length(S))$ 。
+总的时间复杂度为 $O(\left|S\right|)$ 。
 
 ### 出现次数
 
 > 对于一个给定的文本串 $T$ ,有多组询问,每组询问给一个模式串 $P$ ,回答模式串 $P$ 在字符串 $T$ 中作为子串出现了多少次。
 
-对文本串 $T$ 构造 SAM
+对文本串 $T$ 构造后缀自动机
 
-接下来做预处理:对于自动机中的每个状态 $v$ ,预处理 $cnt[v]$ ,使之等于 $endpos(v)$ 集合的大小。事实上,对应同一状态 $v$ 的所有子串在文本串 $T$ 中的出现次数相同,这相当于集合 $endpos$ 中的位置数。
+接下来做预处理:对于自动机中的每个状态 $v$ ,预处理 $cnt_{v}$ ,使之等于 $endpos(v)$ 集合的大小。事实上,对应同一状态 $v$ 的所有子串在文本串 $T$ 中的出现次数相同,这相当于集合 $endpos$ 中的位置数。
 
 然而我们不能明确的构造集合 $endpos$ ,因此我们只考虑它们的大小 $cnt$ 。
 
-为了计算这些值,我们进行以下操作。对于每个状态,如果它不是通过复制创建的(且它不是初始状态 $t_0$ ),我们将它的 $cnt$ 初始化为 1。然后我们按它们的长度 $len$ 降序遍历所有状态,并将当前的 $cnt[v]$ 的值加到后缀链接上,即:
+为了计算这些值,我们进行以下操作。对于每个状态,如果它不是通过复制创建的(且它不是初始状态 $t_0$ ),我们将它的 $cnt$ 初始化为 1。然后我们按它们的长度 $len$ 降序遍历所有状态,并将当前的 $cnt_{v}$ 的值加到后缀链接指向的状态上,即:
 
 $$
-cnt[link(v)]+=cnt[v]
+cnt_{link(v)}+=cnt_{v}
 $$
 
 这样做每个状态的答案都是正确的。
 
-为什么这是正确的?不是通过复制获得的状态,恰好有 $length(T)$ 个,并且它们中的前 $i$ 个在我们插入前 $i$ 个字符时产生。因此对于每个这样的状态,我们在它被处理时计算它们所对应的位置的数量。因此我们初始将这些状态的 $cnt$ 的值赋为 $1$ ,其它状态的 $cnt$ 值赋为 $0$ 。
+为什么这是正确的?不是通过复制获得的状态,恰好有 $\left|T\right|$ 个,并且它们中的前 $i$ 个在我们插入前 $i$ 个字符时产生。因此对于每个这样的状态,我们在它被处理时计算它们所对应的位置的数量。因此我们初始将这些状态的 $cnt$ 的值赋为 $1$ ,其它状态的 $cnt$ 值赋为 $0$ 。
 
-接下来我们对每一个 $v$ 执行以下操作: $cnt[link(v)]+=cnt[v]$ 。其背后的含义是,如果有一个字符串 $v$ 出现了 $cnt[v]$ 次,那么它的所有后缀也在完全相同的地方结束,即也出现了 $cnt[v]$ 次。
+接下来我们对每一个 $v$ 执行以下操作: $cnt_{link(v)}+=cnt_{v}$ 。其背后的含义是,如果有一个字符串 $v$ 出现了 $cnt_{v}$ 次,那么它的所有后缀也在完全相同的地方结束,即也出现了 $cnt_{v}$ 次。
 
 为什么我们在这个过程中不会重复计数(即把某些位置数了两次)呢?因为我们只将一个状态的位置添加到 **一个** 其它的状态上,所以一个状态不可能以两种不同的方式将其位置重复地指向另一个状态。
 
-因此,我们可以在 $O(length(T))$ 的时间内计算出所有状态的 $cnt$ 的值。
+因此,我们可以在 $O(\left|T\right|)$ 的时间内计算出所有状态的 $cnt$ 的值。
 
-最后回答询问只需要查找值 $cnt[t]$ ,其中 $t$ 为模式串对应的状态,如果该模式串不存在答案就为 $0$ 。单次查询的时间复杂度为 $O(length(P))$ 。
+最后回答询问只需要查找值 $cnt_{t}$ ,其中 $t$ 为模式串对应的状态,如果该模式串不存在答案就为 $0$ 。单次查询的时间复杂度为 $O(\left|P\right|)$ 。
 
 ### 第一次出现的位置
 
 > 给定一个文本串 $T$ ,多组查询。每次查询字符串 $P$ 在字符串 $T$ 中第一次出现的位置( $P$ 的开头位置)。
 
-我们构造一个 SAM。我们对自动机中的所有状态预处理位置 $firstpos$ 。即,对每个状态 $v$ 我们想要找到第一次出现这个状态的末端的位置 $firstpos[v]$ 。换句话说,我们希望先找到每个集合 $endpos$ 中的最小的元素(显然我们不能显式地维护所有 $endpos$ 集合)。
+我们构造一个后缀自动机。我们对 SAM 中的所有状态预处理位置 $firstpos$ 。即,对每个状态 $v$ 我们想要找到第一次出现这个状态的末端的位置 $firstpos[v]$ 。换句话说,我们希望先找到每个集合 $endpos$ 中的最小的元素(显然我们不能显式地维护所有 $endpos$ 集合)。
 
 为了维护 $firstpos$ 这些位置,我们将原函数扩展为 `sa_extend()` 。当我们创建新状态 $cur$ 时,我们令:
 
@@ -409,13 +416,13 @@ $$
 
 (因为值的唯一的其它选项 $firstpos(cur)$ 显然太大了)。
 
-那么查询的答案就是 $firstpos(t)-length(P)+1$ ,其中 $t$ 为对应字符串 $P$ 的状态。单次查询只需要 $O(length(P))$ 的时间。
+那么查询的答案就是 $firstpos(t)-\left|P\right|+1$ ,其中 $t$ 为对应字符串 $P$ 的状态。单次查询只需要 $O(\left|P\right|)$ 的时间。
 
 ### 所有出现的位置
 
 > 问题同上,这一次需要查询文本串 $T$ 中模式串出现的所有位置。
 
-我们还是为文本串 $T$ 构造 SAM 。与上一个问题相似,我们为所有状态计算位置 $firstpos$ 。
+我们还是对文本串 $T$ 构造后缀自动机。与上一个问题相似,我们为所有状态计算位置 $firstpos$ 。
 
 如果 $t$ 为对应于模式串 $T$ 的状态,显然 $firstpos(t)$ 为答案的一部分。需要查找的其它位置怎么办?我们使用了含有字符串 $P$ 的自动机,我们还需要将哪些状态纳入自动机呢?所有对应于以 $P$ 为后缀的字符串的状态。换句话说我们要找到所有可以通过后缀链接到达状态 $t$ 的状态。
 
@@ -431,15 +438,15 @@ $$
 
 ```cpp
 struct state {
-  ... bool is_clone;
+  bool is_clone;
   int first_pos;
-  vector<int> inv_link;
+  std::vector<int> inv_link;
+  // some other variables
 };
 
 // 在构造 SAM 后
-for (int v = 1; v < sz; v++) {
+for (int v = 1; v < sz; v++)
   st[st[v].link].inv_link.push_back(v);
-}
 
 // 输出所有出现位置
 void output_all_occurrences(int v, int P_length) {
@@ -452,21 +459,21 @@ void output_all_occurrences(int v, int P_length) {
 
 > 给定一个字符串 $S$ 和一个特定的字符集,我们要找一个长度最短的没有在 $S$ 中出现过的字符串。
 
-我们在字符串 $S$ 的 SAM 上做动态规划。
+我们在字符串 $S$ 的后缀自动机上做动态规划。
 
-令 $d[v]$ 为节点 $v$ 的答案,即,我们已经处理完了子串的一部分,当前在状态 $v$ ,想找到不连续的转移需要添加的最小字符数量。计算 $d[v]$ 非常简单。如果不存在使用字符集中至少一个字符的转移,则 $d[v]=1$ 。否则添加一个字符是不够的,我们需要求出所有转移中的最小值:
+令 $d_{v}$ 为节点 $v$ 的答案,即,我们已经处理完了子串的一部分,当前在状态 $v$ ,想找到不连续的转移需要添加的最小字符数量。计算 $d_{v}$ 非常简单。如果不存在使用字符集中至少一个字符的转移,则 $d_{v}=1$ 。否则添加一个字符是不够的,我们需要求出所有转移中的最小值:
 
 $$
-d[v]=1+\min_{w(v,\,w,\,c)\in SA}d[w]
+d_{v}=1+\min_{w:(v,w,c)\in SAM}d_{w}
 $$
 
-问题的答案就是 $d[t_0]$ ,字符串可以通过计算过的数组 $d[]$ 逆推回去。
+问题的答案就是 $d_{t_0}$ ,字符串可以通过计算过的数组 $d$ 逆推回去。
 
 ### 两个字符串的最长公共子串
 
 > 给定两个字符串 $S$ 和 $T$ ,求出最长公共子串,公共子串定义为在 $S$ 和 $T$ 中都作为子串出现过的字符串 $X$ 。
 
-我们为字符串 $S$ 构造 SAM
+我们对字符串 $S$ 构造后缀自动机
 
 我们现在处理字符串 $T$ ,对于每一个前缀,都在 $S$ 中寻找这个前缀的最长后缀。换句话说,对于每个字符串 $T$ 中的位置,我们想要找到这个位置结束的 $S$ 和 $T$ 的最长公共子串的长度。
 
@@ -474,9 +481,9 @@ $$
 
 一开始 $v=t_0$ 且 $l=0$ ,即,匹配为空串。
 
-现在我们来描述如何添加一个字符 $T[i]$ 并为其重新计算答案:
+现在我们来描述如何添加一个字符 $T_{i}$ 并为其重新计算答案:
 
--   如果存在一个从 $v$ 到字符 $T[i]$ 的转移,我们只需要转移并让 $l$ 自增一。
+-   如果存在一个从 $v$ 到字符 $T_{i}$ 的转移,我们只需要转移并让 $l$ 自增一。
 -   如果不存在这样的转移,我们需要缩短当前匹配的部分,这意味着我们需要按照后缀链接进行转移:
 
 $$
@@ -485,11 +492,11 @@ $$
 
 与此同时,需要缩短当前长度。显然我们需要将 $l$ 赋值为 $len(v)$ ,因为经过这个后缀链接后我们到达的状态所对应的最长字符串是一个子串。
 
--   如果仍然没有使用这一字符的转移,我们继续重复经过后缀链接并减小 $l$ ,直到我们找到一个转移或到达虚拟状态 $-1$ (这意味着字符 $T[i]$ 根本没有在 $S$ 中出现过,所以我们设置 $v=l=0$ )。
+-   如果仍然没有使用这一字符的转移,我们继续重复经过后缀链接并减小 $l$ ,直到我们找到一个转移或到达虚拟状态 $-1$ (这意味着字符 $T_{i}$ 根本没有在 $S$ 中出现过,所以我们设置 $v=l=0$ )。
 
 问题的答案就是所有 $l$ 的最大值。
 
-这一部分的时间复杂度为 $O(length(T))$ ,因为每次移动我们要么可以使 $l$ 增加一,要么可以在后缀链接间移动几次,每次都减小 $l$ 的值。
+这一部分的时间复杂度为 $O(\left|T\right|)$ ,因为每次移动我们要么可以使 $l$ 增加一,要么可以在后缀链接间移动几次,每次都减小 $l$ 的值。
 
 代码实现:
 
@@ -527,7 +534,7 @@ $$
 T=S_1+D_1+S_2+D_2+\cdots+S_k+D_k.
 $$
 
-然后为字符串 $T$ 构造 SAM 
+然后对字符串 $T$ 构造后缀自动机
 
 现在我们需要在自动机中找到存在于所有字符串 $S_i$ 中的一个字符串,这可以通过使用添加的特殊字符完成。注意如果 $S_j$ 包含了一个子串,则 SAM 中存在一条从包含字符 $D_j$ 的子串而不包含以其它字符 $D_1,\,\ldots,\,D_{j-1},\,D_{j+1},\,\ldots,\,D_k$ 开始的路径。