HanLP — Aho-Corasick DoubleArrayTire 演演算法 ACDAT

2023-11-06 15:01:34

雙陣列字典樹能在O(1)(1是模式串長度)時間內高速完成單串匹配,並且記憶體消耗可控,然而軟肋在於多模式匹配。如果要匹配多個模式串,必須先實現字首查詢,然後頻繁擷取文字字尾才可多匹配。比如 ushers、shers、hers…這樣一份文字要回退掃描多遍,效能較低。既然 AC 自動機的goto表本身就是一棵字典樹,能否利用雙陣列字典樹來實現它呢?如果能用雙陣列字典樹表達 AC自動機,就能集合兩者的優點,得到一種近乎完美的資料結構。

ACDAT的基本原理是替換 AC自動機 的goto表,也可看作為一棵雙陣列字典樹的每個狀態(下標)附上額外的資訊。上節提到, AC自動機 的goto表就是字典樹,只不過 AC自動機 比字典樹多了output 表和fail表。那麼ACDAT的構建原理就是為每個狀態(base[i]和check[i])構建output[i][]和fail[i]。具體說來,分為3步。

  • 構建trie樹,讓終止節點記住對應模式串的字典序。

    即將所有模式串構建為一顆字典樹,同時將終止狀態繫結外部value。在實現上可以先用TreeMap簡單實現。

  • 構建雙陣列字典樹,在將每個狀態對映到雙陣列時,讓它記住自己在雙陣列中的下標。

    與單獨構建雙陣列Trie樹不同,在為一個trie樹State建立base[i]的時候,讓該State記住自己的i,這樣就建立State和下標的對映。

  • 構建AC自動機,此時fail表中儲存的就是狀態的下標。

    在構建AC自動機時,每構建一個節點State的fail表,就利用上述對映下標State.id將fail[id]設為failState.id。對於output表,也是同理。

返回所有匹配到的模式串

/**
 * 匹配母文字
 *
 * @param text 一些文字
 * @return 一個pair列表
 */
public List<Hit<V>> parseText(String text)

其中Hit是一個表示命中結果的結構:

/**
 * 一個命中結果
 *
 * @param <V>
 */
public class Hit<V>
{
    /**
     * 模式串在母文字中的起始位置
     */
    public final int begin;
    /**
     * 模式串在母文字中的終止位置
     */
    public final int end;
    /**
     * 模式串對應的值
     */
    public final V value;
}

即時處理
AhoCorasickDoubleArrayTrie提供即時處理的結構:

/**
 * 處理文字
 *
 * @param text      文字
 * @param processor 處理器
 */
public void parseText(String text, IHit<V> processor)

其中IHit<V>是一個輕便的介面:

/**
 * 命中一個模式串的處理方法
 */
public interface IHit<V>
{
    /**
     * 命中一個模式串
     *
     * @param begin 模式串在母文字中的起始位置
     * @param end   模式串在母文字中的終止位置
     * @param value 模式串對應的值
     */
    void hit(int begin, int end, V value);
}

呼叫方法

import com.hankcs.hanlp.collection.AhoCorasick.AhoCorasickDoubleArrayTrie;
import com.hankcs.hanlp.dictionary.CoreDictionary;
import com.hankcs.hanlp.utility.LexiconUtility;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;


public static void main(String[] args) throws IOException {
TreeMap<String, String> map = new TreeMap<>();
    String[] keyArray = new String[]
        {
            "清華",
            "清華大學",
            "清新",
            "中華",
            "華人"
        };
    for (String key : keyArray) {
        map.put(key, key);
    }
    AhoCorasickDoubleArrayTrie<String> act = new AhoCorasickDoubleArrayTrie<>();
    act.build(map);
    act.parseText("清華大學生都是華人", new AhoCorasickDoubleArrayTrie.IHit<String>() {
        @Override
        public void hit(int begin, int end, String value) {
            System.out.printf("[%d:%d]=%s\n", begin, end, value);
        }
    });
}

輸出:

[0:2]=清華
[0:4]=清華大學
[7:9]=華人

單獨的AhoCorasickDoubleArrayTrie類庫:https://github.com/hankcs/AhoCorasickDoubleArrayTrie