Algoritma Rabin-Karp menyelesaikan string matching dengan pendekatan berbeda dari KMP atau Z: alih-alih membandingkan karakter, ia membandingkan hash dari pattern dengan hash setiap window text. Karena hash window bisa diperbarui dalam waktu O(1) dengan teknik rolling hash, algoritma ini rata-rata O(n + m). Rabin-Karp juga secara natural memperluas ke pencarian banyak pattern sekaligus — cukup bandingkan hash window dengan hash dari semua pattern. Pada Chapter ini kita akan memahami rolling hash polynomial, membangun struktur prefix hash untuk akses O(1) ke substring apapun, dan menerapkannya pada search maupun substring equality.
Aturan: Bandingkan Hash, Bukan Karakter #
Rabin-Karp menggunakan polynomial rolling hash:
H(s) = (s[0] * BASE^(m-1) + s[1] * BASE^(m-2) + ... + s[m-1]) mod MBASE: konstanta > ukuran alphabet (mis. 131 untuk ASCII).M: bilangan prima besar (mis.2^61 - 1Mersenne prime).- Hash window
T[i..i+m-1]dapat dihitung dari hash window sebelumnyaT[i-1..i+m-2]dalamO(1).
Mekanisme: Rolling Hash #
Misal H_i = hash(T[i..i+m-1]). Maka:
H_{i+1} = ((H_i - T[i] * BASE^(m-1)) * BASE + T[i+m]) mod M- Kurangi kontribusi karakter kiri yang keluar (
T[i]). - Geser sisanya satu posisi (kali
BASE). - Tambahkan karakter kanan baru (
T[i+m]). - Ambil modulo
Msetiap langkah.
Bila hash window sama dengan hash pattern, lakukan verifikasi karakter per karakter untuk menyingkirkan spurious hit (false positive akibat collision).
Kode: Rolling Hash dengan Prefix Array #
Alih-alih menggeser hash satu per satu, lebih fleksibel membangun prefix hash h[i] = hash(T[0..i-1]). Hash substring T[l..r) dapat diakses dalam O(1):
hash(T[l..r)) = h[r] - h[l] * BASE^(r-l) mod M#include <iostream>
#include <string>
#include <vector>
typedef unsigned long long u64;
const u64 MOD = (1ULL << 61) - 1; // Mersenne prime
const u64 BASE = 131;
u64 mul_mod(u64 a, u64 b) {
__uint128_t r = (__uint128_t)a * b;
u64 lo = (u64)r;
u64 hi = (u64)(r >> 64);
lo = (lo & MOD) + (lo >> 61) + (hi << 3);
if (lo >= MOD) lo -= MOD;
return lo;
}
u64 add_mod(u64 a, u64 b) {
u64 r = a + b;
if (r >= MOD) r -= MOD;
return r;
}
u64 sub_mod(u64 a, u64 b) {
return a >= b ? a - b : a + MOD - b;
}
struct RollingHash {
std::vector<u64> h; // prefix hash
std::vector<u64> p; // powers of BASE
int n;
RollingHash(const std::string& s) {
n = static_cast<int>(s.size());
h.assign(n + 1, 0);
p.assign(n + 1, 0);
p[0] = 1;
for (int i = 0; i < n; ++i) {
p[i + 1] = mul_mod(p[i], BASE);
h[i + 1] = add_mod(mul_mod(h[i], BASE), (u64)(unsigned char)s[i]);
}
}
u64 hash(int l, int r) const {
u64 a = h[r];
u64 b = mul_mod(h[l], p[r - l]);
return sub_mod(a, b);
}
};
int main() {
std::string S = "abcabcabc";
RollingHash rh(S);
std::cout << "S = " << S << '\n';
std::cout << "hash(0,3) == hash(3,6)? "
<< (rh.hash(0, 3) == rh.hash(3, 6) ? "ya" : "tidak") << '\n';
std::cout << "hash(0,3) == hash(1,4)? "
<< (rh.hash(0, 3) == rh.hash(1, 4) ? "ya" : "tidak") << '\n';
return 0;
}Output yang Dihasilkan:
S = abcabcabc
hash(0,3) == hash(3,6)? ya
hash(0,3) == hash(1,4)? tidakMOD = (1ULL << 61) - 1: Mersenne prime2^61 - 1. Operasi modulo dapat dioptimasi dengan bit-shift (lihatmul_mod).p[i]=BASE^i mod M: precompute pangkat BASE. Diperlukan untuk rumus hash substring.h[i + 1] = mul_mod(h[i], BASE) + s[i]: recurrence prefix hash.h[i]adalah hashs[0..i-1].mul_moddengan__uint128_t: mengalikan dua bilangan 64-bit tanpa overflow. Untuk Mersenne prime, reduksi cepat dengan shift.hash(l, r) = sub_mod(h[r], mul_mod(h[l], p[r - l])): hash substrings[l..r). Bila sama untuk dua substring → kemungkinan besar substring sama (collision sangat jarang untukM = 2^61 - 1).- Hasil:
"abc" == "abc"(hash sama)."abc" != "bca"(hash berbeda).
Kode: Rabin-Karp Search #
#include <iostream>
#include <string>
#include <vector>
typedef unsigned long long u64;
const u64 MOD = (1ULL << 61) - 1;
const u64 BASE = 131;
u64 mul_mod(u64 a, u64 b) {
__uint128_t r = (__uint128_t)a * b;
u64 lo = (u64)r;
u64 hi = (u64)(r >> 64);
lo = (lo & MOD) + (lo >> 61) + (hi << 3);
if (lo >= MOD) lo -= MOD;
return lo;
}
u64 add_mod(u64 a, u64 b) { u64 r = a + b; if (r >= MOD) r -= MOD; return r; }
u64 sub_mod(u64 a, u64 b) { return a >= b ? a - b : a + MOD - b; }
struct RollingHash {
std::vector<u64> h, p;
int n;
RollingHash(const std::string& s) {
n = static_cast<int>(s.size());
h.assign(n + 1, 0);
p.assign(n + 1, 0);
p[0] = 1;
for (int i = 0; i < n; ++i) {
p[i + 1] = mul_mod(p[i], BASE);
h[i + 1] = add_mod(mul_mod(h[i], BASE), (u64)(unsigned char)s[i]);
}
}
u64 hash(int l, int r) const {
return sub_mod(h[r], mul_mod(h[l], p[r - l]));
}
};
std::vector<int> rabin_karp(const std::string& T, const std::string& P) {
int n = static_cast<int>(T.size());
int m = static_cast<int>(P.size());
std::vector<int> result;
if (m == 0 || m > n) return result;
RollingHash rh_T(T), rh_P(P);
u64 hp = rh_P.hash(0, m);
for (int i = 0; i + m <= n; ++i) {
if (rh_T.hash(i, i + m) == hp) {
bool match = true;
for (int j = 0; j < m; ++j) {
if (T[i + j] != P[j]) { match = false; break; }
}
if (match) result.push_back(i);
}
}
return result;
}
int main() {
std::string T = "GEEKS FOR GEEKS";
std::string P = "GEEK";
auto occ = rabin_karp(T, P);
std::cout << "T = " << T << '\n';
std::cout << "P = " << P << '\n';
std::cout << "Indeks: ";
for (int idx : occ) std::cout << idx << ' ';
std::cout << '\n';
return 0;
}Output yang Dihasilkan:
T = GEEKS FOR GEEKS
P = GEEK
Indeks: 0 10 u64 hp = rh_P.hash(0, m);: hash pattern dihitung sekali. Bandingkan dengan hash setiap window text.rh_T.hash(i, i + m) == hp: cek hash window diT[i..i+m). Bila sama, kandidat occurrence.- Verifikasi karakter per karakter: cek langsung
T[i + j] == P[j]. Diperlukan karena ada kemungkinan spurious hit (collision). UntukM = 2^61 - 1, collision probability ~n/M— sangat kecil untuk input praktis. - Indeks
0dan10: dua kemunculan"GEEK"di"GEEKS FOR GEEKS".
Kompleksitas #
| Tahap | Best/Average | Worst |
|---|---|---|
| Precompute hash pattern | O(m) | O(m) |
| Precompute prefix hash text | O(n) | O(n) |
| Scan window + verify | O(n + m) | O(n·m) |
Worst case O(n·m) terjadi ketika banyak spurious hit. Contoh adversarial: T = "AAAAAAAA", P = "AAA" — hash cocok di setiap window, verifikasi penuh m karakter setiap kali. Average case (input acak): collision sangat jarang, verifikasi O(1) expected → total O(n + m).
Collision dan Double Hashing #
Bila tetap khawatir collision, gunakan double hash dengan dua pasangan (BASE1, MOD1) dan (BASE2, MOD2) berbeda. Probability collision = 1 / (MOD1 * MOD2) ≈ 1 / 2^122.
struct DoubleHash {
RollingHash h1, h2;
DoubleHash(const std::string& s) : h1(s, BASE1, MOD1), h2(s, BASE2, MOD2) {}
std::pair<u64, u64> hash(int l, int r) const {
return {h1.hash(l, r), h2.hash(l, r)};
}
};Dua hash sekaligus menjamin tidak ada collision praktis; tidak perlu verifikasi karakter.
Aplikasi Lanjutan #
1. Substring Equality #
Dua substring s[l1..r1) dan s[l2..r2) sama bila r1 - l1 == r2 - l2 dan hash(l1, r1) == hash(l2, r2). Banyak masalah string (longest common prefix, repeated substring) selesai dengan ini dalam O(1) per query.
2. Multiple Pattern Matching #
Bangun hash setiap pattern, simpan di hash set. Saat scanning text, cek apakah hash window ada di set. Kompleksitas O(n + k) untuk k pattern (rata-rata).
3. Plagiarism Detection #
Bagi dokumen menjadi chunk berukuran m byte, hash setiap chunk, lalu bandingkan antar dokumen via hash table.
4. Longest Repeated Substring (Binary Search + Hash) #
Binary search panjang L, lalu cek apakah ada dua substring berbeda panjang L dengan hash sama. O(n log n).
Perbandingan dengan KMP #
| Aspek | KMP | Rabin-Karp |
|---|---|---|
| Worst case | O(n + m) | O(n·m) (collision) |
| Average case | O(n + m) | O(n + m) |
| Multi-pattern | Tidak langsung | Ya (hash set) |
| Substring query | Tidak | Ya (O(1) per query) |
| Memori | O(m) | O(n + m) (prefix array) |
| Implementasi | Loop while | Aritmetika modular |
Pilih KMP bila butuh guaranteed linear dan hanya search. Pilih Rabin-Karp bila butuh substring query berulang atau multi-pattern.
Ringkasan #
- Rabin-Karp menggunakan polynomial rolling hash untuk membandingkan pattern dengan window text.
- Hash window dapat diupdate dalam
O(1)(sliding) atau diakses dalamO(1)dengan prefix hash array. - Average case
O(n + m); worst caseO(n·m)karena spurious hit — verifikasi karakter wajib. - Pilihan
BASE = 131,MOD = 2^61 - 1Mersenne prime memberi collision probability sangat kecil. - Double hash dengan dua
(BASE, MOD)berbeda menghilangkan kebutuhan verifikasi. - Aplikasi: substring equality, multiple pattern, plagiarism detection, longest repeated substring (binary search + hash).
- Lebih fleksibel dari KMP untuk masalah substring query; tidak menjamin worst case linear seperti KMP.