数据结构:后缀自动机

后缀自动机

资料:https://pan.quark.cn/s/43d906ddfa1bhttps://pan.quark.cn/s/90ad8fba8347https://pan.quark.cn/s/d9d72152d3cf

一、后缀自动机的定义

后缀自动机(Suffix Automaton,简称 SAM)是一种压缩存储字符串所有子串 的高效有限状态自动机,能够以 O(n) 的空间和时间复杂度表示字符串的所有后缀(及所有子串),是处理字符串子串相关问题的核心数据结构。

SAM 的核心特征:

  • 压缩性 :通过合并等价状态,仅用 O(n) 个状态和转移边表示长度为 n 的字符串的所有子串(总子串数为 n(n+1)/2,直接存储不可行);
  • 高效性 :构建时间/空间复杂度均为 O(n),支持子串查询、不同子串计数、最长重复子串等问题的线性/对数时间求解。

二、后缀自动机的核心概念

1. 状态(State)

SAM 的每个状态代表一组等价的子串(称为"等价类"),满足:

  • 这些子串在字符串中出现的结束位置集合(endpos) 完全相同;
  • 每个状态关联核心属性:
    • len:该状态表示的子串的最大长度
    • link后缀链接,指向另一个状态,表示当前状态的子串去掉首字符后的等价状态(构成一棵后缀树);
    • trans转移字典,键为字符,值为转移后的状态,表示在当前子串末尾添加该字符后的等价状态。

2. 结束位置集合(endpos)

对于字符串 S 的子串 tendpos(t)tS 中所有结束位置的集合。例如 S = "ababa",子串 "aba"endpos = {2,4}

  • 等价状态:endpos 相同的子串属于同一状态;
  • 状态的 len:该状态所有子串的最大长度,最小长度为 link 状态的 len + 1

3. 后缀链接(link)

后缀链接是 SAM 的核心结构,满足:

  • 若状态 u 的后缀链接指向 v,则 endpos(v) ⊇ endpos(u)
  • 所有状态的后缀链接构成一棵以初始状态(空串状态)为根的树。

4. 初始状态与终止状态

  • 初始状态(起始状态) :表示空串,len = 0link = -1(无父节点);
  • 终止状态:所有能通过后缀链接最终到达初始状态,且对应子串为原字符串后缀的状态(可通过构建时标记)。

三、后缀自动机的构建原理

SAM 的构建采用增量法:逐个添加字符串的字符,动态扩展状态和转移,核心步骤为"新建状态 → 扩展转移 → 分裂状态(若需) → 更新后缀链接"。

核心步骤(增量法)

  1. 新建状态 cur :添加字符 c 时,新建状态 cur,其 len = last.len + 1last 为上一个字符对应的状态)。
  2. 扩展转移 :从 last 出发,沿后缀链接向上遍历,为所有无 c 转移的状态添加指向 cur 的转移,直到初始状态或找到有 c 转移的状态 p
  3. 处理转移冲突 :若 p 不存在(遍历到初始状态),则 cur.link = 初始状态;否则找到 p 通过 c 转移的状态 q
    • q.len = p.len + 1:直接令 cur.link = q
    • q.len > p.len + 1:分裂 qclone 状态(复制 qtranslinkclone.len = p.len + 1),更新 qcurlinkclone,并修正 p 及其后缀链路上状态的 c 转移(指向 clone 而非 q)。
  4. 更新 last :令 last = cur,继续添加下一个字符。

四、后缀自动机的实现示例

python 复制代码
class State:
    def __init__(self):
        self.len = 0  # 状态表示的子串的最大长度
        self.link = -1  # 后缀链接
        self.trans = dict()  # 转移字典: {字符: 状态索引}

class SuffixAutomaton:
    def __init__(self):
        self.size = 1  # 状态总数,初始状态为0
        self.last = 0  # 最后一个状态的索引
        self.states = [State()]  # 状态列表
    
    def sa_extend(self, c):
        """增量添加字符c,扩展SAM"""
        cur = self.size
        self.size += 1
        self.states.append(State())
        self.states[cur].len = self.states[self.last].len + 1
        
        p = self.last
        # 沿后缀链接向上,添加转移
        while p != -1 and c not in self.states[p].trans:
            self.states[p].trans[c] = cur
            p = self.states[p].link
        
        if p == -1:
            # 遍历到初始状态,cur的后缀链接指向初始状态
            self.states[cur].link = 0
        else:
            q = self.states[p].trans[c]
            if self.states[p].len + 1 == self.states[q].len:
                # q是p添加c后的直接状态,cur的后缀链接指向q
                self.states[cur].link = q
            else:
                # 分裂q为clone状态
                clone = self.size
                self.size += 1
                self.states.append(State())
                self.states[clone].len = self.states[p].len + 1
                self.states[clone].trans = self.states[q].trans.copy()
                self.states[clone].link = self.states[q].link
                
                # 更新p及其后缀链路上指向q的转移为clone
                while p != -1 and self.states[p].trans.get(c, -1) == q:
                    self.states[p].trans[c] = clone
                    p = self.states[p].link
                
                # 更新q和cur的后缀链接
                self.states[q].link = clone
                self.states[cur].link = clone
        
        self.last = cur
    
    def build(self, s):
        """构建字符串s的SAM"""
        for c in s:
            self.sa_extend(c)
    
    def count_distinct_substrings(self):
        """统计字符串的不同子串数量"""
        res = 0
        for i in range(1, self.size):
            # 每个状态贡献的子串数 = len[i] - len[link[i]]
            res += self.states[i].len - self.states[self.states[i].link].len
        return res
    
    def is_substring(self, t):
        """判断t是否是原字符串的子串"""
        p = 0  # 从初始状态开始
        for c in t:
            if c not in self.states[p].trans:
                return False
            p = self.states[p].trans[c]
        return True

使用示例

python 复制代码
# 构建SAM
s = "abracadabra"
sam = SuffixAutomaton()
sam.build(s)

# 统计不同子串数量
print("不同子串数量:", sam.count_distinct_substrings())  # 输出 53

# 子串查询
print("是否包含子串 'abra':", sam.is_substring("abra"))  # 输出 True
print("是否包含子串 'xyz':", sam.is_substring("xyz"))    # 输出 False

# 最长重复子串(需额外遍历状态计算)
def longest_repeated_substring(sam):
    max_len = 0
    for i in range(1, sam.size):
        link_len = sam.states[sam.states[i].link].len
        # 重复子串需满足:该状态的子串出现至少两次(endpos大小≥2)
        # 简化版:通过len - link_len判断可能的最大长度(精确判断需统计endpos)
        if sam.states[i].len > max_len and link_len > 0:
            max_len = sam.states[i].len
    return max_len

print("最长重复子串长度:", longest_repeated_substring(sam))  # 输出 4(如"abra")

五、后缀自动机的核心操作与时间复杂度

操作 描述 时间复杂度
构建 SAM 增量添加字符,动态扩展状态 O(n)
子串存在性查询 沿转移边遍历 `O(
不同子串计数 遍历所有状态计算贡献 O(n)
最长重复子串 遍历状态找最大 len O(n)
最长公共子串(两字符串) 构建一个字符串的SAM,遍历另一个字符串 O(n+m)

六、后缀自动机的典型应用

  1. 不同子串计数 :核心公式为 ∑(len[i] - len[link[i]])(所有状态的贡献和);
  2. 子串存在性查询:线性时间判断一个字符串是否是原字符串的子串;
  3. 最长重复子串 :找到最大的 len[i],满足该状态的子串出现至少两次;
  4. 最长公共子串 :对字符串 S 构建 SAM,用字符串 T 遍历 SAM,记录遍历过程中的最大长度;
  5. 子串出现次数统计 :通过拓扑排序统计每个状态的 endpos 大小(子串出现次数);
  6. 多字符串公共子串:构建多个字符串的 SAM,合并状态后求解。

七、后缀自动机与其他字符串结构的对比

数据结构 构建复杂度 空间复杂度 核心优势 适用场景
后缀自动机 较复杂 O(n) 空间最优,支持动态添加 海量字符串的子串问题
后缀数组 中等 O(n log n) 直观,支持LCP问题 重复子串、公共子串
字典树(Trie) 简单 O(n) 前缀匹配高效 前缀查询、词频统计

SAM 的核心优势是空间和时间效率极致,尤其适合处理超长字符串(如百万级字符)的子串问题,缺点是理解和实现难度较高;而后缀数组更直观,适合入门级字符串子串问题。

相关推荐
小尧嵌入式4 小时前
C语言中的面向对象思想
c语言·开发语言·数据结构·c++·单片机·qt
花月C4 小时前
基于Redis的BitMap数据结构实现签到业务
数据结构·数据库·redis
一杯美式 no sugar4 小时前
数据结构——单向无头不循环链表
c语言·数据结构·链表
ss2734 小时前
阻塞队列:三组核心方法全对比
java·数据结构·算法
埃伊蟹黄面4 小时前
算法 --- hash
数据结构·c++·算法·leetcode
fei_sun5 小时前
【数据结构】2025年真题
数据结构
我在人间贩卖青春5 小时前
线性表之队列
数据结构·队列
1024小神5 小时前
swift中 列表、字典、集合、元祖 常用的方法
数据结构·算法·swift
Java水解5 小时前
基于Rust实现爬取 GitHub Trending 热门仓库
数据结构·后端