포스트

[웰노운] KMP 알고리즘

서론

KMP 알고리즘에 대해 알아보자.

본론

KMP에 대한 좋은 글은 인터넷에 많이 있으니, 핵심만 알아보자.

결국 KMP 알고리즘은 우리가 원하는 패턴 문자열 $P$의 prefixsuffix의 최대 길이를 어떻게 구해야 되는지가 관건이다.

문자열 $S[0 \dots n-1]$에 대하여 위의 최대 길이를 구해야 한다.

이러한 길이를 구하기 위해서는 DP를 이용해서 Subproblem으로 쪼갤 수 있다.

먼저 Subproblem을 정의한다. 이를 $\pi[i]$라고 한다.

\[\pi[i] = \max\!\bigl\{\,k \mid 0 \le k \le i,\; S[0..k-1] = S[i-k+1..i]\,\bigr\}.\]

그 다음엔 점화식을 얻어야 하는데, 이 점화식이 여러 DP문제의 스타일과 달라서, 좀 난해한 부분이 있다.

우선, $\pi[i]$와 $\pi[i - 1]$의 연관성을 알 수 있을까?

만약 어떠한 문자열 S에 문자 하나를 붙인다고 쳐보자.

그렇다면, 이 문자가 문자열 S의 최대 길이 prefix의 바로 뒷 idx와 일치하는지, 일치하지 않는지 여부로 분기할 수 있다.

일치하는 경우는 단순히 $\pi[i]$의 값을 1 늘려주면 되므로, 간단하다.

반면, 일치하지 않는 경우를 생각해보자.

그렇다면, 이전에서 봤던 최대 길이 prefix 보다 길이가 작은 prefix의 바로 뒷 idx와 비교해보자.

이는 재귀적으로 가면서.. 문자가 하나도 겹치지 않는다면, 0을 리턴할 것이다.

위 내용을 점화식으로 표현하면 되겠는데, 짚고 넘어가야 할 점이있다.

최대 길이 prefix 보다 길이가 작은 prefix를 어떻게 구해야 할까? 이것이 핵심이다.

깊게 생각을 해보면, 결국 $\pi[\pi[i - 1] - 1]$ 형태로 재귀적으로 파고들어갈 수 있음을 알 수 있다.

abacccaba에 b를 붙인다는 예제로, 이를 이해할 수 있다.

점화식 정리

\[\pi[i] = \begin{cases} 0, & i = 0, \\[6pt] \pi[i-1] + 1, & \text{if } S[i] = S\bigl[\pi[i-1]\bigr], \\[6pt] \pi\!\bigl(\,\pi[i-1]-1\bigr), & \pi[i-1] > 0 \text{ and } S[i] \neq S\bigl[\pi[i-1]\bigr], \\[6pt] 0, & \pi[i-1] = 0 \text{ and } S[i] \neq S[0]. \end{cases}\]

3번째 항목이 재귀식의 형태임에 유의하라.

근데 이를 코드로 옮기는 것은 매우 어려워보인다. 어떻게 옮길 수 있을까?

의사 코드

이를 $O(n)$에 구현할 수 있다. 아니, for문이랑 while문이 안에 있는데 왜 $O(n^2)$가 아니고 $O(n)$이라는 의문점이 존재할 것이다.

왜냐하면, for문과 while문을 따로 분리를 해보면, 각각 $O(n)$의 시간복잡도를 가지는 것을 알 수 있다.

좋다. 이제 의사 코드를 작성해보는데, 의사 코드는 두 포인터를 이용해서도 작성할 수 있으나.

두 포인터의 코드는 어렵기도 하고, 우리는 DP를 이용해 열심히 점화식을 구해왔기에! 이를 이용하여 Bottom-up DP를 짜보자.

따라서 최종 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for (int i = 1; i < p.size(); i++) {
    int j = pi[i - 1];

    while (j > 0 && p[i] != p[j])
        j = pi[j - 1];

    pi[i] = j + int(p[i] == p[j]);
} // pi

vector<int> ans;
for (int idx = 0, len = 0; idx < t.size(); idx++) {
    while (len > 0 && t[idx] != p[len])
        len = pi[len - 1];

    len += int(t[idx] == p[len]);

    if (len == p.size()) ans.push_back(idx - p.size() + 1); // 1-based idx
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.