📜  模式搜索的Aho-Corasick算法(1)

📅  最后修改于: 2023-12-03 15:26:51.046000             🧑  作者: Mango

模式搜索的Aho-Corasick算法

Aho-Corasick算法是一种用于在多个模式串上同时匹配的字符串匹配算法,它是基于Trie树的一种改进算法。该算法可以在O(n)的时间复杂度内在一个给定的文本串上匹配多个模式串。该算法的优点在于能够快速的进行模式串的预处理,匹配过程中也非常高效,适用于处理大量模式串的场景。

Trie树

Trie树是一种基于树的数据结构,用于高效的字符串查找和插入。

Trie树的每个节点都代表一个字符串的前缀,从根节点出发到某个节点所经过的路径上的边所表示的字符便是从根节点到该节点的前缀所表示的字符串。节点上存储的值可以是字符串的终止节点,也可以是字符串的前缀节点,显然将每个字符串插入Trie树的过程就是根据字符串的每个字符构造一个从根节点到该节点的链。

我们可以看下面的例子,构造如下字符串的Trie树:

  • Apple
  • Ant
  • Ape
  • Bat
      (root)
        |
        A
    /   |   \
   p    n    p
  / \   |     \
 p   e  t      l
 |       |      \
 l       |       e
 |       |        |
 e       |        |
 |       |        |

在Trie树中搜索某个模式串的时间复杂度为O(m),其中m为模式串长度。

Aho-Corasick算法

Aho-Corasick算法是一种用于在多个模式串上同时匹配的字符串匹配算法,它是基于Trie树的一种改进算法。Aho-Corasick算法主要分为两个步骤:

  • 构建失败指针
  • 匹配模式串
构建失败指针

Aho-Corasick算法中,每个节点还需要存储其对应的失败结点,失败结点是在匹配过程中的“失配”指针。

具体构造方法可以看下图:

对于每个节点,我们可以沿着父节点的失败指针一直向上寻找,直到找到某个节点的后缀子串在Trie树上同样存在,则将该节点的失败指针指向该节点。如果一直找到根节点都无法匹配,则该节点的失败指针指向根节点。

匹配模式串

匹配过程中,我们从Trie树的根节点开始,遇到一个字符就沿着Trie树上的边向下走,并跟踪一个指针指向字符流S的当前位置。当我们在Trie树上移动时,如果遇到了某个节点没有对应的子节点,则将失败指针用来处理该节点的“失配”。

假设我们要在文本串S中寻找多个模式串:S1, S2, S3, ... Sn,则我们可以先将这些模式串插入到Trie树中,并构建每个节点对应的失败指针。然后我们在Trie树中从根节点开始,将指针移向树的下一个节点。如果指向的节点不是一个关键字节点,我们就沿着失败指针将指针移动到树中的下一个节点。如果在移动过程中,指向的节点为某个关键字节点,我们则可以将文本串S中当前的位置作为关键字S1, S2, S3, ... Sn的一个匹配位置。

Aho-Corasick算法与Trie树的区别在于,它不仅匹配所有的模式串,而且其匹配时间与Trie树上最长的“失配”指针长度有关,而不仅仅与文本串长度有关。因此,Aho-Corasick算法的匹配速度相当快,而且其复杂度仅为O(n+k+m),其中n为文本串S的长度,k为所有模式串中字符的种类数量,m为所有模式串的长度之和。

代码示例

下面是Aho-Corasick算法的C++代码实现:

const int N = 2e5 + 5;  // Trie树的节点数
const int CHARSET_SIZE = 26;  // 字符集大小

struct TrieNode {
  int next[CHARSET_SIZE];   // 路径对应的字符
  int fail;                 // 失败指针
  int end;                  // 是否为某串字符串的结尾节点
  // 其他数据结构,如出现次数
} trie[N];

int tot = 1;  // 节点计数器

void insert(int u, char* s) {
  for (int i = 0; s[i]; ++i) {
    int c = s[i] - 'a';
    if (!trie[u].next[c]) {
      ++tot;
      trie[u].next[c] = tot;
    }
    u = trie[u].next[c];
  }
  trie[u].end = 1;
}

void build_fail_pointer() {
  queue<int> q;

  for (int i = 0; i < CHARSET_SIZE; ++i) {
    int v = trie[1].next[i];
    if (v)
      trie[v].fail = 1, q.push(v);  // 根节点子节点的失败指针指向根节点
  }

  while (!q.empty()) {
    int u = q.front(); q.pop();

    for (int i = 0; i < CHARSET_SIZE; ++i) {
      int v = trie[u].next[i];
      if (!v)
        continue;
      int fail_to = trie[u].fail;
      while (fail_to != 1 && !trie[fail_to].next[i]) {
        fail_to = trie[fail_to].fail;
      }
      if (trie[fail_to].next[i])
        fail_to = trie[fail_to].next[i];

      trie[v].fail = fail_to;

      q.push(v);
    }
  }
}

int match(int u, char* s) {
  int len = strlen(s), ans = 0;
  for (int i = 0; i < len; ++i) {
    int c = s[i] - 'a';
    while (u != 1 && !trie[u].next[c])
      u = trie[u].fail;  // 沿着失败指针重定位当前节点
    if (trie[u].next[c])
      u = trie[u].next[c];
    ans += trie[u].end;
  }
  return ans;
}

int main() {
  int n; scanf("%d", &n);
  char s[105];
  for (int i = 1; i <= n; ++i) {
    scanf("%s", s);
    insert(1, s);
  }
  build_fail_pointer();

  scanf("%s", s);

  printf("%d\n", match(1, s));

  return 0;
}

注意:本示例在代码实现上未对Trie树的节点进行优化,每个节点使用了常数空间,不适用于处理大规模数据。