Unverified Commit be98fd0c authored by ir1d's avatar ir1d Committed by GitHub
Browse files

Merge pull request #1162 from FFjet/patch-3

Update sam.md
parents 272922d9 c00ae82e
Loading
Loading
Loading
Loading
+19 −19
Original line number Diff line number Diff line
@@ -40,27 +40,27 @@ SAM 最简单、也最重要的性质是,它包含关于字符串 $s$ 的所

我们用蓝色表示初始状态,用绿色表示终止状态。

对于字符串 $s=``"$ :
对于字符串 $s="$ :

![](./images/SAM/SA.svg)

对于字符串 $s=``a\!"$ :
对于字符串 $s=a\!"$ :

![](./images/SAM/SAa.svg)

对于字符串 $s=``aa\!"$ :
对于字符串 $s=aa\!"$ :

![](./images/SAM/SAaa.svg)

对于字符串 $s=``ab\!"$ :
对于字符串 $s=ab\!"$ :

![](./images/SAM/SAab.svg)

对于字符串 $s=``abb\!"$ :
对于字符串 $s=abb\!"$ :

![](./images/SAM/SAabb.svg)

对于字符串 $s=``abbb\!"$ :
对于字符串 $s=abbb\!"$ :

![](./images/SAM/SAabbb.svg)

@@ -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 定理我们可以得出最小性(不会在这篇文章中证明)。

@@ -129,7 +129,7 @@ $$

结合前面的引理有:后缀链接构成的树本质上是 $endpos$ 集合构成的一棵树。

以下是对字符串 $``abcbc\!"$ 构造 SAM 时产生的后缀链接树的一个 **例子** ,节点被标记为对应等价类中最长的子串。
以下是对字符串 $abcbc\!"$ 构造 SAM 时产生的后缀链接树的一个 **例子** ,节点被标记为对应等价类中最长的子串。

![](./images/SAM/SA_suffix_links.svg)

@@ -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++;
@@ -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;
@@ -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$ 个状态。

### 转移数

@@ -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\!"$ 就达到了这个上界。

## 应用

@@ -404,7 +404,7 @@ $$

我们构造一个后缀自动机。我们对 SAM 中的所有状态预处理位置 $firstpos$ 。即,对每个状态 $v$ 我们想要找到第一次出现这个状态的末端的位置 $firstpos[v]$ 。换句话说,我们希望先找到每个集合 $endpos$ 中的最小的元素(显然我们不能显式地维护所有 $endpos$ 集合)。

为了维护 $firstpos$ 这些位置,我们将原函数扩展为 `sa_extend()` 。当我们创建新状态 $cur$ 时,我们令:
为了维护 $firstpos$ 这些位置,我们将原函数扩展为 `sam_extend()` 。当我们创建新状态 $cur$ 时,我们令:

$$
firstpos(cur)=len(cur)-1
@@ -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++) {