Loading docs/string/sam.md +19 −19 Original line number Diff line number Diff line Loading @@ -40,27 +40,27 @@ SAM 最简单、也最重要的性质是,它包含关于字符串 $s$ 的所 我们用蓝色表示初始状态,用绿色表示终止状态。 对于字符串 $s=``"$ : 对于字符串 $s=“"$ :  对于字符串 $s=``a\!"$ : 对于字符串 $s=“a\!"$ :  对于字符串 $s=``aa\!"$ : 对于字符串 $s=“aa\!"$ :  对于字符串 $s=``ab\!"$ : 对于字符串 $s=“ab\!"$ :  对于字符串 $s=``abb\!"$ : 对于字符串 $s=“abb\!"$ :  对于字符串 $s=``abbb\!"$ : 对于字符串 $s=“abbb\!"$ :  Loading @@ -70,11 +70,11 @@ SAM 最简单、也最重要的性质是,它包含关于字符串 $s$ 的所 ### 结束位置 `endpos` 考虑字符串 $s$ 的任意非空子串 $t$ ,我们记 $endpos(t)$ 为在字符串 $s$ 中 $t$ 的所有结束位置(假设对字符串中字符的编号从零开始)。例如,对于字符串 $``abcbc\!"$,我们有 $endpos(``bc\!")=2,\,4$。 考虑字符串 $s$ 的任意非空子串 $t$ ,我们记 $endpos(t)$ 为在字符串 $s$ 中 $t$ 的所有结束位置(假设对字符串中字符的编号从零开始)。例如,对于字符串 $“abcbc\!"$,我们有 $endpos(“bc\!")=2,\,4$。 两个子串 $t_1$ 与 $t_2$ 的 $endpos$ 集合可能相等: $endpos(t_1)=endpos(t_2)$ 。这样所有字符串 $s$ 的非空子串都可以根据它们的 $endpos$ 集合被分为若干 **等价类** 。 显然, SAM 中的每个状态对应一个或多个 $endpos$ 相同的子串。换句话说, SAM 中的状态数等于所有子串的等价类的个数,再加上初始状态。 SAM 的状态个数等价于 $endpos$ 相同的一个或多个子串所组成的集合的个数 $+1$ 。 显然, SAM 中的每个状态对应一个或多个 $endpos$ 相同的子串。换句话说, SAM 中的状态数等于所有子串的等价类的个数,再加上初始状态。 SAM 的状态个数等价于 $endpos$ 相同的一个或多个子串所组成的集合的个数 $+1$ 。 我们稍后将会用这个假设来介绍构造 SAM 的算法。我们将发现, SAM 需要满足的所有性质,除了最小性以外都满足了。由 Nerode 定理我们可以得出最小性(不会在这篇文章中证明)。 Loading Loading @@ -129,7 +129,7 @@ $$ 结合前面的引理有:后缀链接构成的树本质上是 $endpos$ 集合构成的一棵树。 以下是对字符串 $``abcbc\!"$ 构造 SAM 时产生的后缀链接树的一个 **例子** ,节点被标记为对应等价类中最长的子串。 以下是对字符串 $“abcbc\!"$ 构造 SAM 时产生的后缀链接树的一个 **例子** ,节点被标记为对应等价类中最长的子串。  Loading Loading @@ -235,7 +235,7 @@ int sz, last; 我们定义一个函数来初始化 SAM (创建一个只有初始状态的 SAM)。 ```cpp void sa_init() { void sam_init() { st[0].len = 0; st[0].link = -1; sz++; Loading @@ -246,7 +246,7 @@ void sa_init() { 最终我们给出主函数的实现:给当前行末增加一个字符,对应地在之前的基础上建造自动机。 ```cpp void sa_extend(char c) { void sam_extend(char c) { int cur = sz++; st[cur].len = st[last].len + 1; int p = last; Loading Loading @@ -288,7 +288,7 @@ void sa_extend(char c) { 然而我们也能在 **不借助这个算法** 的情况下 **证明** 这个估计值。我们回忆一下状态数等于不同的 $endpos$ 集合个数。这些 $endpos$ 集合形成了一棵树(祖先节点的集合包含了它所有孩子节点的集合)。考虑将这棵树稍微变形一下:只要它有一个只有一个孩子的内部结点(这意味着该子节点的集合至少遗漏了它的父集合中的一个位置),我们创建一个含有这个遗漏位置的集合。最后我们可以获得一棵每一个内部结点的度数大于 1 的树,且叶子节点的个数不超过 $n$ 。因此这样的树里有不超过 $2n-1$ 个节点。 字符串 $``abbb\ldots bbb\!"$ 的状态数达到了该上界:从第三次迭代后的每次迭代,算法都会拆开一个状态,最终产生恰好 $2n-1$ 个状态。 字符串 $“abbb\ldots bbb\!"$ 的状态数达到了该上界:从第三次迭代后的每次迭代,算法都会拆开一个状态,最终产生恰好 $2n-1$ 个状态。 ### 转移数 Loading @@ -300,9 +300,9 @@ void sa_extend(char c) { 现在我们来估计不连续的转移的数量。令当前不连续转移为 $(p,\,q)$ ,其字符为 $c$ 。我们取它的对应字符串 $u+c+w$ ,其中字符串 $u$ 对应于初始状态到 $p$ 的最长路径, $w$ 对应于从 $p$ 到任意终止状态的最长路径。一方面,每个不完整的字符串所对应的形如 $u+c+w$ 的字符串是不同的(因为字符串 $u$ 和 $w$ 仅由完整的转移组成)。另一方面,由终止状态的定义,每个形如 $u+c+w$ 的字符串都是整个字符串 $s$ 的后缀。因为 $s$ 只有 $n$ 个非空后缀,且形如 $u+c+w$ 的字符串都不包含 $s$ (因为整个字符串只包含完整的转移),所以非完整的转移的总数不会超过 $n-1$ 。 将以上两个估计值相加,我们可以得到上界 $3n-3$ 。然而,最大的状态数只能在类似于 $``abbb\ldots bbb\!"$ 的情况中产生,而此时转移数量显然少于 $3n-3$ 。 将以上两个估计值相加,我们可以得到上界 $3n-3$ 。然而,最大的状态数只能在类似于 $“abbb\ldots bbb\!"$ 的情况中产生,而此时转移数量显然少于 $3n-3$ 。 因此我们可以获得更为紧确的 SAM 的转移数的上界: $3n-4$ 。字符串 $``abbb\ldots bbbc\!"$ 就达到了这个上界。 因此我们可以获得更为紧确的 SAM 的转移数的上界: $3n-4$ 。字符串 $“abbb\ldots bbbc\!"$ 就达到了这个上界。 ## 应用 Loading Loading @@ -404,7 +404,7 @@ $$ 我们构造一个后缀自动机。我们对 SAM 中的所有状态预处理位置 $firstpos$ 。即,对每个状态 $v$ 我们想要找到第一次出现这个状态的末端的位置 $firstpos[v]$ 。换句话说,我们希望先找到每个集合 $endpos$ 中的最小的元素(显然我们不能显式地维护所有 $endpos$ 集合)。 为了维护 $firstpos$ 这些位置,我们将原函数扩展为 `sa_extend()` 。当我们创建新状态 $cur$ 时,我们令: 为了维护 $firstpos$ 这些位置,我们将原函数扩展为 `sam_extend()` 。当我们创建新状态 $cur$ 时,我们令: $$ firstpos(cur)=len(cur)-1 Loading Loading @@ -503,9 +503,9 @@ $$ 代码实现: ```cpp string lcs(string S, string T) { sa_init(); for (int i = 0; i < S.size(); i++) sa_extend(S[i]); string lcs(const string &S, const string &T) { sam_init(); for (int i = 0; i < S.size(); i++) sam_extend(S[i]); int v = 0, l = 0, best = 0, bestpos = 0; for (int i = 0; i < T.size(); i++) { Loading Loading
docs/string/sam.md +19 −19 Original line number Diff line number Diff line Loading @@ -40,27 +40,27 @@ SAM 最简单、也最重要的性质是,它包含关于字符串 $s$ 的所 我们用蓝色表示初始状态,用绿色表示终止状态。 对于字符串 $s=``"$ : 对于字符串 $s=“"$ :  对于字符串 $s=``a\!"$ : 对于字符串 $s=“a\!"$ :  对于字符串 $s=``aa\!"$ : 对于字符串 $s=“aa\!"$ :  对于字符串 $s=``ab\!"$ : 对于字符串 $s=“ab\!"$ :  对于字符串 $s=``abb\!"$ : 对于字符串 $s=“abb\!"$ :  对于字符串 $s=``abbb\!"$ : 对于字符串 $s=“abbb\!"$ :  Loading @@ -70,11 +70,11 @@ SAM 最简单、也最重要的性质是,它包含关于字符串 $s$ 的所 ### 结束位置 `endpos` 考虑字符串 $s$ 的任意非空子串 $t$ ,我们记 $endpos(t)$ 为在字符串 $s$ 中 $t$ 的所有结束位置(假设对字符串中字符的编号从零开始)。例如,对于字符串 $``abcbc\!"$,我们有 $endpos(``bc\!")=2,\,4$。 考虑字符串 $s$ 的任意非空子串 $t$ ,我们记 $endpos(t)$ 为在字符串 $s$ 中 $t$ 的所有结束位置(假设对字符串中字符的编号从零开始)。例如,对于字符串 $“abcbc\!"$,我们有 $endpos(“bc\!")=2,\,4$。 两个子串 $t_1$ 与 $t_2$ 的 $endpos$ 集合可能相等: $endpos(t_1)=endpos(t_2)$ 。这样所有字符串 $s$ 的非空子串都可以根据它们的 $endpos$ 集合被分为若干 **等价类** 。 显然, SAM 中的每个状态对应一个或多个 $endpos$ 相同的子串。换句话说, SAM 中的状态数等于所有子串的等价类的个数,再加上初始状态。 SAM 的状态个数等价于 $endpos$ 相同的一个或多个子串所组成的集合的个数 $+1$ 。 显然, SAM 中的每个状态对应一个或多个 $endpos$ 相同的子串。换句话说, SAM 中的状态数等于所有子串的等价类的个数,再加上初始状态。 SAM 的状态个数等价于 $endpos$ 相同的一个或多个子串所组成的集合的个数 $+1$ 。 我们稍后将会用这个假设来介绍构造 SAM 的算法。我们将发现, SAM 需要满足的所有性质,除了最小性以外都满足了。由 Nerode 定理我们可以得出最小性(不会在这篇文章中证明)。 Loading Loading @@ -129,7 +129,7 @@ $$ 结合前面的引理有:后缀链接构成的树本质上是 $endpos$ 集合构成的一棵树。 以下是对字符串 $``abcbc\!"$ 构造 SAM 时产生的后缀链接树的一个 **例子** ,节点被标记为对应等价类中最长的子串。 以下是对字符串 $“abcbc\!"$ 构造 SAM 时产生的后缀链接树的一个 **例子** ,节点被标记为对应等价类中最长的子串。  Loading Loading @@ -235,7 +235,7 @@ int sz, last; 我们定义一个函数来初始化 SAM (创建一个只有初始状态的 SAM)。 ```cpp void sa_init() { void sam_init() { st[0].len = 0; st[0].link = -1; sz++; Loading @@ -246,7 +246,7 @@ void sa_init() { 最终我们给出主函数的实现:给当前行末增加一个字符,对应地在之前的基础上建造自动机。 ```cpp void sa_extend(char c) { void sam_extend(char c) { int cur = sz++; st[cur].len = st[last].len + 1; int p = last; Loading Loading @@ -288,7 +288,7 @@ void sa_extend(char c) { 然而我们也能在 **不借助这个算法** 的情况下 **证明** 这个估计值。我们回忆一下状态数等于不同的 $endpos$ 集合个数。这些 $endpos$ 集合形成了一棵树(祖先节点的集合包含了它所有孩子节点的集合)。考虑将这棵树稍微变形一下:只要它有一个只有一个孩子的内部结点(这意味着该子节点的集合至少遗漏了它的父集合中的一个位置),我们创建一个含有这个遗漏位置的集合。最后我们可以获得一棵每一个内部结点的度数大于 1 的树,且叶子节点的个数不超过 $n$ 。因此这样的树里有不超过 $2n-1$ 个节点。 字符串 $``abbb\ldots bbb\!"$ 的状态数达到了该上界:从第三次迭代后的每次迭代,算法都会拆开一个状态,最终产生恰好 $2n-1$ 个状态。 字符串 $“abbb\ldots bbb\!"$ 的状态数达到了该上界:从第三次迭代后的每次迭代,算法都会拆开一个状态,最终产生恰好 $2n-1$ 个状态。 ### 转移数 Loading @@ -300,9 +300,9 @@ void sa_extend(char c) { 现在我们来估计不连续的转移的数量。令当前不连续转移为 $(p,\,q)$ ,其字符为 $c$ 。我们取它的对应字符串 $u+c+w$ ,其中字符串 $u$ 对应于初始状态到 $p$ 的最长路径, $w$ 对应于从 $p$ 到任意终止状态的最长路径。一方面,每个不完整的字符串所对应的形如 $u+c+w$ 的字符串是不同的(因为字符串 $u$ 和 $w$ 仅由完整的转移组成)。另一方面,由终止状态的定义,每个形如 $u+c+w$ 的字符串都是整个字符串 $s$ 的后缀。因为 $s$ 只有 $n$ 个非空后缀,且形如 $u+c+w$ 的字符串都不包含 $s$ (因为整个字符串只包含完整的转移),所以非完整的转移的总数不会超过 $n-1$ 。 将以上两个估计值相加,我们可以得到上界 $3n-3$ 。然而,最大的状态数只能在类似于 $``abbb\ldots bbb\!"$ 的情况中产生,而此时转移数量显然少于 $3n-3$ 。 将以上两个估计值相加,我们可以得到上界 $3n-3$ 。然而,最大的状态数只能在类似于 $“abbb\ldots bbb\!"$ 的情况中产生,而此时转移数量显然少于 $3n-3$ 。 因此我们可以获得更为紧确的 SAM 的转移数的上界: $3n-4$ 。字符串 $``abbb\ldots bbbc\!"$ 就达到了这个上界。 因此我们可以获得更为紧确的 SAM 的转移数的上界: $3n-4$ 。字符串 $“abbb\ldots bbbc\!"$ 就达到了这个上界。 ## 应用 Loading Loading @@ -404,7 +404,7 @@ $$ 我们构造一个后缀自动机。我们对 SAM 中的所有状态预处理位置 $firstpos$ 。即,对每个状态 $v$ 我们想要找到第一次出现这个状态的末端的位置 $firstpos[v]$ 。换句话说,我们希望先找到每个集合 $endpos$ 中的最小的元素(显然我们不能显式地维护所有 $endpos$ 集合)。 为了维护 $firstpos$ 这些位置,我们将原函数扩展为 `sa_extend()` 。当我们创建新状态 $cur$ 时,我们令: 为了维护 $firstpos$ 这些位置,我们将原函数扩展为 `sam_extend()` 。当我们创建新状态 $cur$ 时,我们令: $$ firstpos(cur)=len(cur)-1 Loading Loading @@ -503,9 +503,9 @@ $$ 代码实现: ```cpp string lcs(string S, string T) { sa_init(); for (int i = 0; i < S.size(); i++) sa_extend(S[i]); string lcs(const string &S, const string &T) { sam_init(); for (int i = 0; i < S.size(); i++) sam_extend(S[i]); int v = 0, l = 0, best = 0, bestpos = 0; for (int i = 0; i < T.size(); i++) { Loading