본문으로 건너뛰기
김신건의 로그

Aho-Corasick (아호-코라식)

· 수정 · 📖 약 2분 · 766자/단어 #algorithm #string #aho-corasick #pattern-matching #trie
aho-corasick, Aho-Corasick, 아호-코라식, 다중 패턴 매칭

정의

Aho-Corasick 은 Alfred Aho 와 Margaret Corasick 이 1975년 고안한 다중 패턴 문자열 매칭 알고리즘. TrieKMP failure function 을 결합해 여러 패턴을 한 번의 텍스트 순회로 모두 찾는다. O(N + M + matches) (N = 텍스트 길이, M = 총 패턴 길이).

문제 상황과 동기

악성코드 시그니처 k 개를 네트워크 패킷 T 에서 모두 탐지해야 한다.

  • naive: 각 패턴마다 KMP → O(k x (N + |pi|)). k = 10^4, N = 10^6 이면 10^10.
  • Aho-Corasick: 한 번의 Trie 순회. O(N + M + matches).

핵심 통찰: Trie 에서 mismatch 시 failure link 를 따라가면, 이미 스캔한 접미사 중 가장 긴 패턴 접두사로 즉시 이동. KMP 의 π 를 Trie 로 일반화.

시각화

핵심 아이디어

1. Trie 구축

모든 패턴을 Trie 에 삽입. 각 노드에 output (패턴 완성) 표시.

루트 직계 자식 → fail = 루트
BFS:
  for 자식 u of v (c):
    f = fail[v]
    while f && !trie[f][c]: f = fail[f]
    if trie[f][c]: f = trie[f][c]
    fail[u] = f
    output[u] |= output[f]

3. 매칭

텍스트를 한 문자씩 읽으며 Trie 이동. 현재 노드나 failure link 에 output 이 있으면 매치.

invariant: 현재 노드 = 텍스트 접미사 중 가장 긴, 어떤 패턴의 접두사.

알고리즘

insert(s):
    v = root
    for ch in s:
        if !trie[v][c]: trie[v][c] = new_node()
        v = trie[v][c]
    term[v] = true

build():
    q = queue(root 의 자식)
    while q:
        v = q.pop()
        for c in 0..25:
            u = trie[v][c]; if !u: continue
            f = fail[v]
            while f && !trie[f][c]: f = fail[f]
            if trie[f][c]: f = trie[f][c]
            fail[u] = f; term[u] |= term[f]; q.push(u)

search(text):
    v = 0; cnt = 0
    for ch in text:
        c = ch - 'a'
        while v && !trie[v][c]: v = fail[v]
        if trie[v][c]: v = trie[v][c]
        if term[v]: cnt++
    return cnt

구현

// Aho-Corasick, O(N+M) - Trie + failure link
#include <bits/stdc++.h>
using namespace std;
const int MAX = 10000;
int trie[MAX][26], fail[MAX], term[MAX], idx;
int main() {
  ios::sync_with_stdio(false); cin.tie(nullptr);
  int n; cin >> n;
  for (int i = 0; i < n; i++) {
      string s; cin >> s; int v = 0;
      for (char ch : s) { int c = ch - 'a';
          if (!trie[v][c]) trie[v][c] = ++idx;
          v = trie[v][c]; }
      term[v] = 1;
  }
  queue<int> q;
  for (int c = 0; c < 26; c++)
      if (trie[0][c]) q.push(trie[0][c]);
  while (!q.empty()) {
      int v = q.front(); q.pop();
      for (int c = 0; c < 26; c++) {
          int u = trie[v][c]; if (!u) continue;
          int f = fail[v];
          while (f && !trie[f][c]) f = fail[f];
          if (trie[f][c]) f = trie[f][c];
          fail[u] = f; term[u] |= term[f]; q.push(u);
      }
  }
  string text; cin >> text; int v = 0, cnt = 0;
  for (char ch : text) { int c = ch - 'a';
      while (v && !trie[v][c]) v = fail[v];
      if (trie[v][c]) v = trie[v][c];
      if (term[v]) cnt++; }
  cout << cnt << "\n";
}
stdin
5
he
she
his
hers
her
hershe
결과
4

복잡도

항목
전처리O(M) 시간, O(M x Σ) 공간
매칭O(N + matches) 시간
전체O(N + M + matches)

while 로 failure link 를 거슬러 올라가는 비용은 amortized O(M) (BFS 각 노드 1회, 검색 중 v 는 감소 후 증가로 상쇄).

변형 / 활용

1. goto / failure 최적화

trie[v][c] 가 0 이면 trie[fail[v]][c] 로 미리 연결. 검색 while 문 제거 → O(N) strictly.

2. 출력 위치 저장

term 대신 패턴 ID 리스트 저장. 각 노드가 어떤 패턴의 끝인지, failure link 로 전파되는 패턴까지 관리.

3. 2D 격자 패턴 매칭, 악성코드 탐지

Snort / YARA 의 핵심 엔진.

함정

term[v] |= term[fail[v]] 누락 시 “she” 내부의 “he” 를 놓침.

2. 배열 크기

Trie 배열 = Σ|patterns| x Σ 까지. 동적 할당(vector) 추천.

3. 충분하지 않은 while

while 로 failure link 를 끝까지 올라가도 amortized O(N). 최적화로 제거 가능.

BOJ 연습 문제

번호제목정답률링크
BOJ 9250문자열 집합 판별40.3%kokoa-lab
BOJ 10256돌연변이31.2%kokoa-lab
BOJ 9935문자열 폭발33.1%kokoa-lab
BOJ 16916부분 문자열34.7%kokoa-lab

참고

이 글의 용어 (3개)
문자열 (String)algorithm
정의 문자열 (String) 은 문자(character)의 순서 있는 나열. 프로그래밍에서는 연속된 메모리에 저장된 문자 배열 로 구현되며, 각 문자는 ASCII(7비트) 또는 …
트라이 (Trie, 접두사 트리)algorithm
정의 트라이 (Trie) 는 Edward Fredkin 이 1960년 제안한 문자열 집합을 저장하는 트리 자료구조. 각 노드는 문자 하나, 간선은 다음 문자로의 전이. 루트에서 …
KMP 문자열 매칭 (Knuth-Morris-Pratt)algorithm
정의 KMP (Knuth-Morris-Pratt) 는 Donald Knuth, James H. Morris, Vaughan Pratt 가 1977년 고안한 선형 시간 문자열 매칭…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기