字串不能直接拿來當陣列的 key;但把字串映射成一個整數,就可以。
字串比較為什麼慢?
判斷兩個子字串是否相同,暴力做法是逐字元比對——O(m),m 是子字串長度。
如果你在解「最長公共子字串」,需要枚舉所有長度和起始位置,每次比對 O(m),總共 O(n²m)——在 n = 10^4 時就超時了。
Rolling Hash 的思路:把每個子字串映射成一個整數。整數比較是 O(1),雜湊值相同則子字串大概率相同。
多項式雜湊
把字串看成多項式的係數:
hash("abc") = 'a' × p² + 'b' × p¹ + 'c' × p⁰ (mod m)
p = base,通常選 131(比 26 大的質數)
m = modulus,通常選大質數 10⁹+7 或 10⁹+9
前綴雜湊:預先計算每個前綴的雜湊值,然後 O(1) 取任意子字串:
class RollingHash {
long[] hash, pow;
static final long BASE = 131L, MOD = 1_000_000_007L;
RollingHash(String s) {
int n = s.length();
hash = new long[n + 1];
pow = new long[n + 1];
pow[0] = 1;
for (int i = 0; i < n; i++) {
hash[i + 1] = (hash[i] * BASE + s.charAt(i)) % MOD;
pow[i + 1] = pow[i] * BASE % MOD;
}
}
long get(int l, int r) { // [l, r] 閉區間
return (hash[r + 1] - hash[l] * pow[r - l + 1] % MOD + MOD) % MOD;
}
}get(l, r) 的原理:
hash[r+1] = hash[l] × p^(r-l+1) + subHash
↓
subHash = hash[r+1] - hash[l] × p^(r-l+1)
就像前綴和:sum(l, r) = prefix[r+1] - prefix[l]。
雙雜湊:消滅碰撞風險
單一雜湊有碰撞問題——不同字串可能有相同雜湊值,導致誤判。實務上用兩組不同的 (BASE, MOD):
boolean equal(int l1, int r1, int l2, int r2) {
return h1.get(l1, r1) == h1.get(l2, r2)
&& h2.get(l1, r1) == h2.get(l2, r2);
}兩個獨立雜湊同時碰撞的機率約 1 / (10⁹ × 10⁹)——競程中實際上不會發生。
應用:最長公共子字串
找兩個字串的最長公共子字串,暴力 O(n²m),用 Rolling Hash + 二分搜尋:
二分搜尋長度 L:
若存在長度 L 的公共子字串,則更長的也值得找
→ 單調性成立,可以二分
驗證長度 L 是否可行:
對 s1 的每個長度 L 的子字串算雜湊,存入 Set
對 s2 的每個長度 L 的子字串算雜湊,查詢是否在 Set 中
→ O(n) 時間
總時間:O(n log n)
Rabin-Karp 字串匹配也是同樣思路——pattern 的雜湊和滑動窗口的雜湊比較,O(n + m) 匹配,只在雜湊相同時才做 O(m) 的精確驗證。
Rolling Hash 是一種空間換時間的預處理——把字串轉成整數的代價是一次 O(n) 建表,換來之後每次 O(1) 的子字串比較。