무엇을 해결하나
KMP는 한 패턴을 본문에서 찾는다. 그런데 패턴이 여러 개(\(P_1, \dots, P_k\))
이고, 긴 본문 \(T\)에서 그중 어느 것이든 등장하는 위치를 모두 찾으려면?
각 패턴마다 KMP를 돌리면 \(O(|T| \cdot k)\)로 느리다. 아호-코라식(Aho-Corasick)
은 이를 \(O(|T| + \sum |P_i| + \text{등장 수})\)에 처리한다.
트라이 + 실패 링크
핵심은 두 도구의 결합이다.
- 트라이(trie): 모든 패턴을 글자 단위로 하나의 접두사 트리에 넣는다. 어떤
패턴이 끝나는 노드에 "여기서 패턴이 끝난다"는 표시를 단다. - 실패 링크(failure link): 본문을 한 글자씩 읽다가 현재 노드에서 다음 글자로
못 갈 때, 현재까지 읽은 접미사 중 트라이에 존재하는 가장 긴 접두사로
점프하는 링크. KMP의 실패 함수를 트라이 전체로 일반화한 것이다.
실패 링크의 정의: 노드 \(v\)(루트에서 \(v\)까지 문자열 \(s\))의 실패 링크는,
\(s\)의 진 접미사 중 트라이에 존재하는 가장 긴 것에 대응하는 노드.
실패 링크는 BFS로
실패 링크는 트라이 위에서 BFS(레벨 순) 로 계산한다. 깊이 1 노드의 실패
링크는 루트다. 노드 \(u\)에서 글자 \(c\)로 가는 자식 \(v\)의 실패 링크는
"\(u\)의 실패 링크에서 \(c\)로 갈 수 있는 노드"로 정한다 — KMP의 실패 함수 점화와
정확히 같은 구조다. 자식이 없으면 그 방향 전이를 실패 링크의 전이로 채우는
goto 함수(자동자화) 를 쓰면 본문 처리 시 항상 \(O(1)\)로 다음 상태로 간다.
출력 링크
한 위치에서 끝나는 패턴이 여러 개일 수 있다(예: "she"와 "he"). 그래서 실패
링크를 타고 올라가며 만나는 패턴 끝 노드들을 모두 보고해야 한다. 매번
올라가면 느리므로, 실패 링크 중 가장 가까운 패턴 끝 노드로 바로 가는
출력 링크(dict suffix link) 를 미리 계산해 둔다.
복잡도
트라이 구축 \(O(\sum |P_i|)\), 실패/출력 링크 BFS도 같은 비용, 본문 처리는 한
글자당 \(O(1)\) 전이라 \(O(|T|)\), 거기에 실제 등장 보고 비용을 더한다. KMP가
"한 패턴 자동자"라면 아호-코라식은 "패턴 집합 자동자"다.