"aaaaaaaaaaab" 안에 "aaaab" 가 존재하는지 찾아보자.
일반적인 방법 (브루트포스) 이라면, 우선 첫 번째 요소부터 시작해서 5개를 추출하고, 또 다시 그 다섯개와 목표 문자열을 하나하나 비교할 것이다.
이렇게 마지막까지 반복하고 최종적으로 의 시간복잡도를 가진다.
(문자열의 길이: , 부분문자열의 길이: )
라빈 카프는 이 작업을 에 할수 있게 해준다.
라빈 카프의 개념은 슬라이딩 윈도우 / 투포인터 와 비슷하다. 전체를 계산하는 것이 아니라 움직일때 마다 값을 하나 추가하거나 뺀다.
(: 해시 값, : 문자열의 i 번째 문자, : 해싱의 기준이 되는 소수, : 해시 값을 나누는 큰 소수, : C문자의 길이)
식은 복잡하니 예시로 이해해 보자.
(, 일 때)
"abcd" 라는 문자열의 해시를 라빈 카프 알고리즘으로 구하면 다음과 같은 값이 나온다.
다음으로, "bcde" 의 해시 값을 롤링 해시를 사용해서 구해보자.
1. "abcd" 의 해시값에서 "a"에 해당 하는 해시 값을 빼고
2. 전체에 10을 곱하고
3. "e"에 대한 해시값을 더하면 된다.
4. 중간 중간에 mod 연산을 해줘야 음수가 되지 않는다.
확인 해보면 로 맞다.
위와 같은 연산을 작용하면, 부분 문자열이 아무리 길어도 의 계산으로 다음 부분문자열의 해시값을 알 수 있다.
위에 예시에서는 과 에 임의로 과 이라는 숫자를 사용했다.
과 는 아무 값이나 가능하지만, 큰 소수를 사용해야 해시 충돌 확률이 작다.
1,000,000,007 (10억 7) 또는 1,234,567,891 같은 수를 사용한다.100,000,000,000,031 이 있다.)백준 3033 - 부분 문자열 찾기, 라빈 카프 알고리즘 연습문제
위 문제는 lcp 알고리즘 또는 이분 탐색 + 라빈 카프 알고리즘으로 풀린다.
자바의 구현체인 HashMap 으로는 문자열의 최대 길이가 200,000 이므로 모든 부분 문자열을 저장해 버리면 메모리 초과가 난다. 그래서 해싱으로 풀려면 필수적으로 해싱 알고리즘을 구현해야하는 문제이다.
라빈-카프 알고리즘으로 풀 경우 값을 매우 크게 잡지 않으면 해싱 충돌이 일어나 실패가 뜨니까 주의하자.
이 문제에 파라매트릭 서치 (이분 탐색)를 사용한다.
import java.util.*;
import java.io.*;
public class Main {
private static final long MOD = 100_000_000_000_031L;
private static final int X = 31;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int l = Integer.parseInt(br.readLine());
String str = br.readLine();
int start = 1;
int end = l - 1;
int ans = 0;
while (start <= end) {
int mid = (start + end) / 2;
if (isValid(str, mid)) {
ans = mid;
start = mid + 1;
} else {
end = mid - 1;
}
}
System.out.println(ans);
}
private static boolean isValid(String str, int x) {
Set<Long> set = new HashSet<>();
long hash = 0;
long X_power = 1;
for (int i = 0; i < x; i++) {
hash = (hash * X + str.charAt(i)) % MOD;
if (i > 0) {
X_power = (X_power * X) % MOD;
}
}
set.add(hash);
// 매번 해시값을 계산하는게 아니라, 필요한 값만 뺴고 더하면서 구한다.
for (int i = x; i < str.length(); i++) {
hash = (hash - (str.charAt(i - x) * X_power) % MOD + MOD) % MOD;
hash = (hash * X + str.charAt(i)) % MOD;
if (set.contains(hash)) {
return true;
}
set.add(hash);
}
return false;
}
}
isValid() 함수가 핵심이다.
첫 번째 for문에서 맨 처음 부분문자열의 해시 값을 구한 후,
두번째 for문에서 다음 부분문자열의 값을 구하면서 로직이 작동된다.
레퍼런스: Rolling Hash Function Tutorial, used by Rabin-Karp String Searching Algorithm
참고 - 간단한 해싱 알고리즘으로는 DJB2 가 있습니다. 단, 롤링 해시는 불가능 합니다.