Map each string to a number so that comparing substrings becomes . Simple, general, and the fastest route to a working solution for most string problems — provided you defend against anti-hash tests.
Polynomial hashing
Precompute prefix hashes and powers, then any substring hash is :
or, avoiding the inverse, compare against .
struct Hashing {
static const long long M = (1LL << 61) - 1; // Mersenne prime
long long B;
vector<long long> h, p;
static long long mul(long long a, long long b) { // mod 2^61-1 without __int128 division
__uint128_t r = (__uint128_t)a * b;
long long lo = (long long)(r & M), hi = (long long)(r >> 61);
long long s = lo + hi;
return s >= M ? s - M : s;
}
Hashing(const string& s, long long base) : B(base), h(s.size()+1, 0), p(s.size()+1, 1) {
for (size_t i = 0; i < s.size(); i++) {
h[i+1] = (mul(h[i], B) + s[i]) % M;
p[i+1] = mul(p[i], B);
}
}
long long get(int l, int r) const { // inclusive [l, r]
long long res = (h[r+1] - mul(h[l], p[r-l+1])) % M;
return res < 0 ? res + M : res;
}
};Note this version builds the hash left to right so that get needs no inverse — the cleaner formulation.
Defending against collisions
Anti-hash tests are real
Codeforces problems routinely include tests built to break
M = 10^9+7with a fixed base. The Thue-Morse sequence breaks any base modulo in length; birthday attacks find collisions for a fixed pair in tries.
Four defences, in order of effectiveness:
- Randomise the base at runtime.
B = uniform_int_distribution<long long>(256, M-2)(rng)with a time-seeded RNG. This alone defeats every precomputed test. - Use . A birthday attack needs candidates, which is out of reach inside a submission.
- Double hashing. Two independent pairs, compared as a pair. Effectively .
- Never use
unsigned long longoverflow () — it is broken by Thue-Morse strings regardless of the base.
With a random base modulo , the collision probability over comparisons is about — negligible.
What hashing buys you
| Task | Method |
|---|---|
| Substring equality | compare hashes, |
| Compare substrings lexicographically | binary search the LCP, then compare one character — |
| Pattern matching | rolling hash over the text |
| Count distinct substrings of length | hash every window, count distinct |
| Longest common substring of two strings | binary search the length + hash sets, |
| Longest palindromic substring | binary search with forward and reverse hashes |
| Is a palindrome? | compare forward and reverse hashes, |
| Longest common prefix of two suffixes | binary search on the hash |
| Match a pattern with wildcards | hashing plus FFT, or per-position checks |
| Tree hashing | the same idea on tree structures |
Palindrome checking in
Keep a second hash of the reversed string. Then is a palindrome iff its forward hash equals the corresponding reverse-string hash — a two-line addition that replaces Manacher for many problems.
Hashing vs the deterministic alternatives
| Hashing | Suffix automaton / array | |
|---|---|---|
| Code length | ~30 lines | 40-90 lines |
| Correctness | probabilistic | exact |
| Substring compare | after build | |
| Count distinct substrings | ||
| Memory | to | |
| Anti-test risk | real, mitigable | none |
| Generality | very high | high |
Hashing is the right first tool. Switch to a suffix automaton when you need to enumerate or count over all substrings, which hashing cannot do efficiently.
Hashing beyond strings
The same polynomial trick hashes any sequence: arrays of integers, tree shapes, multisets (sum of over elements, with a random scramble), and 2D grids (hash rows, then hash the row hashes).
See also: Rolling Hash · Rabin-Karp · Suffix Automaton