代表性面试主题

编程面试:如何实现 Suffix Automaton 并解释 clone 状态?

编程题困难
Offer.cc 编辑团队发布 更新

题干

请实现 Suffix Automaton 的 extend 操作,解释 suffix link、clone 和线性状态上界,并给出一个实际查询。

题目

给定字符串 s,在线构造 Suffix Automaton(SAM)。实现 extend(c),说明每个状态的 lenlink、转移和 endpos 含义,并用它计算不同子串数量、模式出现次数或最长公共子串。要求解释为什么状态数是 O(n),以及何时必须创建 clone。

面试官考察点

  • 能否把一个状态解释为一组具有相同 endpos 的子串,而不是把它当作普通 Trie 节点。
  • 能否正确区分连续转移、直接挂接和 clone 三种构造分支。
  • 能否维护 len[link[v]] < len[v]、转移确定性和 suffix-link 树不变量。
  • 能否把结构性质转化为计数、匹配和复杂度结论。

参考答案

SAM 是识别原串所有子串的最小部分 DFA。状态 v 保存该等价类中最长子串长度 len[v]link[v] 指向最长的、属于另一个等价类的后缀。状态代表的长度区间是 (len[link[v]], len[v]],因此一个状态可以覆盖多个不同长度的子串。

追加字符时先创建 cur,沿 suffix link 向后寻找缺少该转移的状态并补边。若遇到已有状态 qlen[p]+1 == len[q],直接令 link[cur]=q;若不相等,复制 q 的转移创建 clone,把 clone 的 len 设为 len[p]+1,再沿 suffix link 把指向 q 的相关边改到 clone,并令 link[q]link[cur] 都指向 clone。clone 保证每个状态的长度区间连续。

所有非根状态对不同子串的贡献是 len[v] - len[link[v]]。若先给原串前缀对应状态计数 1,再按 len 降序把计数累加到 link,即可得到每个等价类的出现次数。

实现示例

下面的伪代码展示核心构造;转移可以用哈希表或有序映射。

text
extend(c):
  cur = new state
  len[cur] = len[last] + 1
  p = last
  while p != -1 and c not in next[p]:
    next[p][c] = cur
    p = link[p]
  if p == -1:
    link[cur] = root
  else:
    q = next[p][c]
    if len[p] + 1 == len[q]:
      link[cur] = q
    else:
      clone = copy(q)
      len[clone] = len[p] + 1
      while p != -1 and next[p][c] == q:
        next[p][c] = clone
        p = link[p]
      link[q] = link[cur] = clone
  last = cur

计算不同子串数量时遍历非根状态累加 len[v] - len[link[v]]。计算模式出现次数时沿转移走到对应状态,再读取按长度降序传播后的计数。

常见误区

  • 把 SAM 当成只接受后缀的普通 Trie,忽略它实际压缩了所有子串的 endpos 等价类。
  • clone 复制了转移却忘记复制或重新设置 link,导致后续长度区间断裂。
  • 修改转移时没有沿 suffix link 回溯到第一个不再指向 q 的状态。
  • 给 clone 计入一次前缀出现次数,造成所有出现次数偏大。
  • 用固定数组存储巨大字符集,却没有说明空间成本和字符编码边界。

复杂度取舍

在固定字母表或哈希转移下,构造时间和空间为 O(n),状态最多约 2n-1。若每个状态的转移使用平衡树,复杂度会增加到与字母表操作相关的对数因子。SAM 适合固定文本上的大量子串查询;需要字典序遍历、LCP 或后缀排序时,后缀数组可能更易控制内存和访问局部性。

SAM 的线性上界依赖在线追加模型。若要在字符串中间插入、删除或双端更新,通常需要不同结构,不能直接沿用 extend 的不变量。

先用空串、单字符、重复字符和触发 clone 的 abbb 做逐步断言,再随机生成字符串,与暴力集合比较不同子串数量和模式出现次数。最长公共子串测试可把第二个字符串逐字符送入 SAM,并在失配时沿 suffix link 回退。

参考资料

  • CP-algorithms 的 SAM 讲义:状态区间、clone 构造和查询公式。
  • Blumer 等人的最小子串自动机论文:状态与转移上界的理论来源。
  • Carnegie Mellon 字符串算法讲义:后缀数组与后缀自动机的适用场景比较。

追问

为什么每个状态代表连续长度区间?

同一 endpos 等价类中的子串按长度排列时不会出现空洞;最长子串的后缀会逐步落入包含它的更大等价类。link 指向区间左侧的边界,因此区间正好是 (len[link[v]], len[v]]

什么时候必须创建 clone?

当已有转移目标 qlen[q] 大于 len[p]+1 时,q 同时承载了两个不连续的长度范围。复制转移并把边界拆出 clone,才能恢复连续区间不变量。

为什么不同子串数量是这些区间长度之和?

每个状态覆盖的长度区间不重叠,且区间中的每个长度对应一个不同子串。把每个状态的区间大小相加,就得到全部非空不同子串数。

SAM 与 Aho–Corasick 如何选择?

SAM 面向一个文本的全部子串结构和多种统计;Aho–Corasick 面向一组已知模式在文本中的批量匹配。需求是模式集合固定还是文本结构固定,决定了构造方向。

如何扩展到最长公共子串?

先为字符串 S 建 SAM,再扫描 T。沿转移前进并维护当前匹配长度;失配时沿 suffix link 回退并继续尝试,记录过程中出现的最大长度即可。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

截图题目后,按顺序看约束、解法、代码、边界条件和复杂度。

查看工具