AC 自动机模板

一句话说明

AC 自动机就是“Trie + fail 指针”,用一次扫描同时匹配多条模式串。

模板适用场景

  • 文本里要匹配多组关键词
  • 一次扫描找出所有模式串出现位置
  • 模式串数量很多,不适合逐条 KMP

Go 模板

type ACNode struct {
    Next   map[byte]int
    Fail   int
    Output []int
}
 
type AhoCorasick struct {
    Nodes    []ACNode
    Patterns []string
}
 
func NewAhoCorasick(patterns []string) *AhoCorasick {
    ac := &AhoCorasick{
        Nodes: []ACNode{{Next: map[byte]int{}}},
        Patterns: patterns,
    }
 
    for id, pattern := range patterns {
        ac.insert(pattern, id)
    }
    ac.build()
    return ac
}
 
func (ac *AhoCorasick) newNode() int {
    ac.Nodes = append(ac.Nodes, ACNode{Next: map[byte]int{}})
    return len(ac.Nodes) - 1
}
 
func (ac *AhoCorasick) insert(pattern string, id int) {
    state := 0
    for i := 0; i < len(pattern); i++ {
        ch := pattern[i]
        next, ok := ac.Nodes[state].Next[ch]
        if !ok {
            next = ac.newNode()
            ac.Nodes[state].Next[ch] = next
        }
        state = next
    }
    ac.Nodes[state].Output = append(ac.Nodes[state].Output, id)
}
 
func (ac *AhoCorasick) build() {
    queue := []int{}
    for _, next := range ac.Nodes[0].Next {
        queue = append(queue, next)
    }
 
    for len(queue) > 0 {
        state := queue[0]
        queue = queue[1:]
 
        for ch, child := range ac.Nodes[state].Next {
            fail := ac.Nodes[state].Fail
            for fail > 0 {
                if _, ok := ac.Nodes[fail].Next[ch]; ok {
                    break
                }
                fail = ac.Nodes[fail].Fail
            }
 
            if next, ok := ac.Nodes[fail].Next[ch]; ok {
                ac.Nodes[child].Fail = next
            }
            ac.Nodes[child].Output = append(ac.Nodes[child].Output, ac.Nodes[ac.Nodes[child].Fail].Output...)
            queue = append(queue, child)
        }
    }
}
 
func (ac *AhoCorasick) Search(text string) [][2]interface{} {
    state := 0
    matches := [][2]interface{}{}
 
    for i := 0; i < len(text); i++ {
        ch := text[i]
        for state > 0 {
            if _, ok := ac.Nodes[state].Next[ch]; ok {
                break
            }
            state = ac.Nodes[state].Fail
        }
 
        if next, ok := ac.Nodes[state].Next[ch]; ok {
            state = next
        }
 
        for _, patternID := range ac.Nodes[state].Output {
            pattern := ac.Patterns[patternID]
            matches = append(matches, [2]interface{}{i - len(pattern) + 1, pattern})
        }
    }
 
    return matches
}

为什么 fail 指针要按 BFS 构造

因为一个节点的 fail,依赖它父节点的 fail 已经先求出来。
这正是 BFS 层序构造的意义。

为什么输出要继承 fail 节点

因为当前模式串的后缀,可能本身又是另一个模式串。
如果不继承 fail 节点的输出,就会漏掉这些“后缀匹配”。

易错点

AC 自动机模板最容易错的地方

  • fail 必须按 BFS 顺序构造。
  • 输出集合要继承 fail 指向节点的输出。
  • 模式串集合一旦变化,通常要整体重建自动机。

复杂度

阶段时间复杂度
建 TrieO(P)
建 failO(P)
扫描文本O(T + Z)

这里 P 是模式串总长度,T 是文本长度,Z 是匹配结果数。

相关主题


返回:算法模板