라빈-카프 알고리즘 (Rabin-Karp)
정의
라빈-카프 (Rabin-Karp) 는 롤링 해시 (rolling hash) 를 이용해 패턴 문자열 P[0..M-1] 이 텍스트 T[0..N-1] 의 어디에 등장하는지 찾는 문자열 매칭 알고리즘. 1987 년 Rabin, Karp 가 고안. 평균 O(N + M), 최악 O(NM) 이지만 실제로는 충돌이 거의 없어 매우 빠르다.
문제 상황과 동기
길이 M 패턴이 길이 N 텍스트의 어디에 나타나는가?
- naive: 모든 위치에서 M 글자 비교. O(NM). N=10^6, M=10^5 면 10^11 연산.
- KMP/Z: O(N + M) 보장, 하지만 전처리 복잡.
- Rabin-Karp: 해시 함수 H(s) 로 패턴 해시 H(P) 를 계산 → 텍스트의 모든 M 길이 부분 문자열 해시를 O(1) 롤링 갱신. 해시 일치 시 실제 비교.
핵심 통찰: “슬라이딩 윈도우” 의 해시를 O(1) 에 갱신. polynomial hash → 첫 문자 빼고 뒤 문자 추가 → modulo 상수 시간.
자주 등장: 다중 패턴 매칭, 2D 패턴 (이미지 검색), plagiarism 검출.
시각화
핵심 아이디어
문자열 s[i..i+M-1] 의 해시를 다항식으로 정의:
H(s[i..i+M-1]) = (s[i]·B^(M-1) + s[i+1]·B^(M-2) + ... + s[i+M-1]·B^0) mod MOD
B 는 진법 (보통 31, 37, 256 등), MOD 는 큰 소수 (1e9+7, 1e9+9, 2^61-1).
롤링 갱신 (i → i+1 슬라이딩):
H(s[i+1..i+M]) = (H(s[i..i+M-1]) - s[i]·B^(M-1)) · B + s[i+M] (mod MOD)
O(1) 에 다음 윈도우 해시 계산. 패턴 해시 H(P) 와 비교 → 일치 시 실제 비교 (해시 충돌 검증).
amortized O(N + M): N 개 윈도우 해시 각 O(1), 충돌 거의 없으면 실제 비교 O(M) 이 상수 번.
알고리즘
rabin_karp(T[0..N-1], P[0..M-1]):
hP = hash(P)
hT = hash(T[0..M-1])
power = B^(M-1) mod MOD
for i = 0..N-M:
if hT == hP:
if T[i..i+M-1] == P:
report match at i
if i < N - M:
hT = ((hT - T[i] * power) * B + T[i+M]) mod MOD
구현
// 라빈-카프 패턴 매칭, 평균 O(N + M)
#include <bits/stdc++.h>
using namespace std;
const long long B = 31, MOD = 1e9 + 7;
long long hash_str(const string& s) {
long long h = 0;
for (char c : s) h = (h * B + (c - 'a' + 1)) % MOD;
return h;
}
vector<int> rabin_karp(const string& T, const string& P) {
int N = T.size(), M = P.size();
if (M > N) return {};
long long hP = hash_str(P), hT = hash_str(T.substr(0, M));
long long power = 1;
for (int i = 0; i < M - 1; i++) power = (power * B) % MOD;
vector<int> matches;
for (int i = 0; i <= N - M; i++) {
if (hT == hP && T.substr(i, M) == P) matches.push_back(i);
if (i < N - M) {
hT = ((hT - (T[i] - 'a' + 1) * power % MOD + MOD) % MOD * B
+ (T[i + M] - 'a' + 1)) % MOD;
}
}
return matches;
}
int main() {
string T, P;
cin >> T >> P;
auto res = rabin_karp(T, P);
cout << res.size() << "\n";
for (int pos : res) cout << pos << " ";
if (!res.empty()) cout << "\n";
}ababcababc
abc2
2 7복잡도
| 항목 | 값 |
|---|---|
| 시간 (평균) | O(N + M) |
| 시간 (최악) | O(NM) - 해시 충돌 시 모든 매칭 재비교 |
| 공간 | O(1) 추가 (해시값 저장) |
| 전처리 | O(M) - 패턴 해시 + B^(M-1) 계산 |
최악 케이스는 모든 윈도우 해시가 패턴 해시와 같지만 실제 문자열은 다를 때. 하지만 좋은 MOD (큰 소수) 와 B 선택 시 충돌 확률 < 1/MOD.
변형 / 활용
1. 다중 패턴 매칭
패턴 K 개 P_1, ..., P_K 를 모두 찾기. 패턴 해시 K 개를 set 에 저장 → 텍스트 해시 O(N) 스캔 → set 조회 O(log K) → 총 O(N log K + Σ M_i).
2. 2D 패턴 매칭
이미지 (2D 배열) 에서 템플릿 찾기. 각 행을 해시 → 열 방향 롤링 해시. O(HW) (H, W 는 텍스트 크기).
3. double hashing
충돌 확률 줄이기 위해 두 modulo (MOD1, MOD2) 동시 사용. 둘 다 일치 시 실제 비교. 충돌 확률 < 1/(MOD1 · MOD2).
함정
1. 음수 해시
hT - (T[i] - 'a' + 1) * power 가 음수 될 수 있다. + MOD 후 다시 mod 필요.
hT = ((hT - val * power % MOD + MOD) % MOD * B + next) % MOD;
2. 해시 충돌
작은 MOD (예: 1e6+3) 는 충돌 많음. 1e9+7, 1e9+9, 2^61-1 같은 큰 소수 사용.
3. B 선택
B=26 은 알파벳만 구별 → 충돌 많음. B=31, 37 추천. 대소문자 구별 시 B=256.
4. overflow
power * B 계산 시 long long overflow. (power * B) % MOD 반드시.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 16916 | 부분 문자열 | - | kokoa-lab |
| BOJ 13022 | 늑대와 올바른 단어 | - | kokoa-lab |
| BOJ 1786 | 찾기 | - | kokoa-lab |
참고
이 글의 용어 (3개)
- 문자열 해싱 (String Hashing)algorithm
- 정의 문자열 해싱 (String Hashing) 은 문자열을 고정 길이의 정수 (해시값) 로 매핑하여, 부분 문자열 비교를 O(1) 에 수행할 수 있게 만드는 기법. Rabin과…
- KMP 문자열 매칭 (Knuth-Morris-Pratt)algorithm
- 정의 KMP (Knuth-Morris-Pratt) 는 Donald Knuth, James H. Morris, Vaughan Pratt 가 1977년 고안한 선형 시간 문자열 매칭…
- Z 알고리즘 (Z Algorithm)algorithm
- 정의 Z 알고리즘 (Z Algorithm) 은 문자열 의 각 위치 i 에 대해, 와 의 최장 공통 접두사 (LCP) 길이 를 O(N) 시간에 모두 계산하는 선형 알고리즘. 결과 …
💬 댓글