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

KMP 문자열 매칭 (Knuth-Morris-Pratt)

· 수정 · 📖 약 4분 · 1,335자/단어 #algorithm #string #kmp #pattern-matching
kmp, KMP, Knuth-Morris-Pratt, KMP 알고리즘

정의

KMP (Knuth-Morris-Pratt) 는 Donald Knuth, James H. Morris, Vaughan Pratt 가 1977년 고안한 선형 시간 문자열 매칭 알고리즘. 텍스트 T 에서 패턴 P 의 모든 출현을 O(N+M) 에 찾는다.

핵심은 failure function π 를 전처리(O(M))하여, mismatch 시 이미 매치된 접두사를 재사용해 불필요한 비교를 건너뛰는 것.

문제 상황과 동기

텍스트 T (길이 N), 패턴 P (길이 M). P 가 T 에 출현하는 모든 위치를 찾는다.

  • naive: T 의 각 위치에서 P 를 처음부터 비교. worst-case O(N×M). N=M=10^5 면 10^10 → TLE.
  • KMP: failure function π 를 활용해 mismatch 시 텍스트 포인터 i 를 되돌리지 않고 패턴 포인터 j 만 조정. 각 문자 최대 1회 비교 → O(N+M).

핵심 통찰: 패턴의 접두사와 접미사가 겹치는 부분을 미리 계산하면, mismatch 시 “이미 확인한 부분”을 재사용 가능.

자주 등장하는 위치: 로그 파싱, DNA 서열 분석, 텍스트 에디터의 find, 압축 알고리즘 (LZ77).

시각화

핵심 아이디어

failure function π[i]: 패턴 P[0..i]proper prefix 이면서 동시에 suffix 인 최대 길이.

P = "ABABC"
π = [0, 0, 1, 2, 0]

π[3] = 2 의미: P[0..3] = "ABAB" 의 최장 proper prefix-suffix = "AB" (길이 2)

매칭 과정:

  1. 텍스트 포인터 i, 패턴 포인터 j 모두 0 에서 시작.
  2. T[i] == P[j] 이면 둘 다 증가.
  3. T[i] != P[j] 이면:
    • j > 0j ← π[j-1] (패턴을 π 만큼 시프트)
    • j = 0i++ (텍스트만 진행)
  4. j = M 이면 매치 발견 (시작 위치 = i - M). 계속 찾으려면 j ← π[j-1].

invariant: i 는 절대 감소하지 않음 (각 문자 최대 1회 읽기). j 는 π 를 따라 감소하지만, π 의 총합이 O(M) 이므로 amortized O(1).

알고리즘

failure function 계산

compute_failure(P):
    M = len(P)
    π[0] = 0
    j = 0
    for i = 1 .. M-1:
        while j > 0 and P[i] != P[j]:
            j = π[j-1]
        if P[i] == P[j]:
            j++
        π[i] = j
    return π

복잡도: O(M). while 루프는 amortized O(1) (j 는 최대 M 번만 증가).

KMP 매칭

kmp_search(T, P):
    N = len(T), M = len(P)
    π = compute_failure(P)
    j = 0
    matches = []
    for i = 0 .. N-1:
        while j > 0 and T[i] != P[j]:
            j = π[j-1]
        if T[i] == P[j]:
            j++
        if j == M:
            matches.append(i - M + 1)
            j = π[j-1]
    return matches

복잡도: O(N+M). while 루프도 amortized O(1).

구현

// KMP 문자열 매칭, O(N+M) - failure function π 기반
#include <bits/stdc++.h>
using namespace std;

// π[i] = P[0..i] 의 최장 proper prefix-suffix 길이
vector<int> compute_failure(const string& P) {
  int M = P.size();
  vector<int> pi(M, 0);
  int j = 0;
  for (int i = 1; i < M; i++) {
      while (j > 0 && P[i] != P[j]) j = pi[j-1];
      if (P[i] == P[j]) j++;
      pi[i] = j;
  }
  return pi;
}

vector<int> kmp_search(const string& T, const string& P) {
  int N = T.size(), M = P.size();
  vector<int> pi = compute_failure(P);
  vector<int> matches;
  int j = 0;
  for (int i = 0; i < N; i++) {
      while (j > 0 && T[i] != P[j]) j = pi[j-1];
      if (T[i] == P[j]) j++;
      if (j == M) {
          matches.push_back(i - M + 1);  // 0-indexed 시작 위치
          j = pi[j-1];
      }
  }
  return matches;
}

int main() {
  string T, P;
  getline(cin, T);
  getline(cin, P);
  auto matches = kmp_search(T, P);
  cout << matches.size() << "\n";
  for (int pos : matches) cout << pos << " ";
  if (!matches.empty()) cout << "\n";
  return 0;
}
stdin
ABABDABACDABABCABAB
ABABC
결과
1
10

복잡도

항목
failure function 전처리O(M) 시간, O(M) 공간
매칭O(N) 시간, O(1) 공간 (π 는 전처리 포함)
전체O(N + M)
공간O(M) (π 배열)

증명 (amortized):

  • i 는 0 → N-1 로 정확히 N 번 증가.
  • j 는 최대 M 번 증가 (failure 에서 감소하지만, j++ 총합 ≤ M).
  • while 루프 (j = π[j-1]) 는 j 를 감소시키지만, j 의 총 증가량이 M 이므로 총 감소도 ≤ M.
  • 따라서 총 연산 = O(N + M).

변형 / 활용

1. 다중 패턴 매칭

여러 패턴을 찾으려면 Aho-Corasick (KMP + Trie).

2. 순환 문자열 매칭

문자열 S 가 순환 문자열인지 판단: S + S 에서 S 를 KMP 로 찾기. 출현 위치 < N 이면 순환.

3. 주기 찾기

len - π[len-1] 이 문자열의 최소 주기. 예: “ABCABCABC”, π[8]=6 → 주기 = 9-6 = 3 (“ABC”).

4. 회문 (Palindrome) 판별

KMP 를 역방향으로 적용하거나 Manacher 사용.

함정

1. π[0] 초기화

π[0] 은 항상 0. proper prefix 는 자기 자신을 제외.

2. j 범위 체크

while 루프 조건 j > 0 필수. j = 0 일 때 π[-1] 접근하면 segfault.

3. 매칭 후 j 조정

매치 발견 후 j ← π[j-1] 로 reset 필요. 안 하면 다음 매치를 놓침.

4. 1-indexed vs 0-indexed

출력 위치가 1-indexed 면 i - M + 2 또는 별도 처리.

5. 빈 패턴

M = 0 이면 모든 위치가 매칭. 별도 예외 처리 필요.

BOJ 연습 문제

번호제목정답률링크
BOJ 1786찾기31.2%kokoa-lab
BOJ 4354문자열 제곱28.5%kokoa-lab
BOJ 1305광고22.1%kokoa-lab
BOJ 16916부분 문자열34.7%kokoa-lab

참고

이 글의 용어 (6개)
라빈-카프 알고리즘 (Rabin-Karp)algorithm
정의 라빈-카프 (Rabin-Karp) 는 롤링 해시 (rolling hash) 를 이용해 패턴 문자열 이 텍스트 의 어디에 등장하는지 찾는 문자열 매칭 알고리즘. 1987 년 …
매내처 알고리즘 (Manacher's Algorithm)algorithm
정의 매내처 알고리즘 (Manacher's Algorithm) 은 문자열 의 모든 위치에서 중심으로 하는 최장 회문 (palindrome) 반지름 을 O(N) 시간에 계산하는 선…
문자열 (String)algorithm
정의 문자열 (String) 은 문자(character)의 순서 있는 나열. 프로그래밍에서는 연속된 메모리에 저장된 문자 배열 로 구현되며, 각 문자는 ASCII(7비트) 또는 …
트라이 (Trie, 접두사 트리)algorithm
정의 트라이 (Trie) 는 Edward Fredkin 이 1960년 제안한 문자열 집합을 저장하는 트리 자료구조. 각 노드는 문자 하나, 간선은 다음 문자로의 전이. 루트에서 …
Aho-Corasick (아호-코라식)algorithm
정의 Aho-Corasick 은 Alfred Aho 와 Margaret Corasick 이 1975년 고안한 다중 패턴 문자열 매칭 알고리즘. Trie 에 KMP failure …
Z 알고리즘 (Z Algorithm)algorithm
정의 Z 알고리즘 (Z Algorithm) 은 문자열 의 각 위치 i 에 대해, 와 의 최장 공통 접두사 (LCP) 길이 를 O(N) 시간에 모두 계산하는 선형 알고리즘. 결과 …

💬 댓글

사이트 검색 / 명령어

검색

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