Randomized hashes compress structured objects (strings, sets, multisets, positions) into fingerprints for comparison or dictionary lookup, and random test points verify identities that are expensive to compute. Hash equality is evidence, not proof: budget the error over all comparisons, and keep the randomness hidden from the adversary. Polynomial string hashing in depth is in Hashing.
| Check | Method | Error |
|---|---|---|
| two multisets equal | sum of random 64-bit weights (mod ) | per comparison |
| every value occurs an even number of times | XOR of random weights (Zobrist) | |
| subarray is a permutation of | sum hash against the hash of | |
| for matrices | Freivalds: test for random | per round with a 0/1 vector, with a random vector mod |
| polynomial identically zero, two expressions equal | evaluate at random points (Schwartz–Zippel) | $\le \frac{d}{ |
| a perfect matching exists | random Tutte matrix, rank | : General Matching |
| two rooted trees isomorphic | random polynomial tree hash | Tree Isomorphism |
Verifying is easier than computing: Freivalds checks a matrix product in instead of , and randomness closes the gap.
Set hashing and Zobrist hashing
Assign each value a random 64-bit weight; hash a multiset by the sum of the weights (mod ) and a set-with-parity by the XOR. Updates are , so prefix arrays give subarray hashes. Uses: comparing multisets, board positions in game engines (Zobrist hashing), tree hashing, “do these two ranges contain the same multiset”, counting subarrays where every value occurs an even number of times (pairs of equal prefix XORs), or a multiple of times (a base- digit hash).
struct SetHashing { // values a[i] in [0, maxValue]
vector<ull> weight, prefSum, prefXor, permSum;
SetHashing(const vector<int>& a, int maxValue, ull seed) {
mt19937_64 gen(seed); weight.resize(maxValue + 1);
for (auto& w : weight) w = gen();
int n = sz(a); prefSum.assign(n + 1, 0); prefXor.assign(n + 1, 0);
for (int i = 0; i < n; i++) prefSum[i + 1] = prefSum[i] + weight[a[i]], prefXor[i + 1] = prefXor[i] ^ weight[a[i]];
permSum.assign(maxValue + 2, 0); // permSum[k] = weight[1] + ... + weight[k]
for (int k = 1; k <= maxValue; k++) permSum[k] = permSum[k - 1] + weight[k];
}
ull sumHash(int l, int r) const { return prefSum[r] - prefSum[l]; } // multiset of a[l, r)
ull xorHash(int l, int r) const { return prefXor[r] ^ prefXor[l]; } // values with odd count
bool isPermutation(int l, int r) const { int k = r - l; return k < sz(permSum) && sumHash(l, r) == permSum[k]; }
};
ll countEvenCountSubarrays(const SetHashing& h, int n) { // subarrays where every value occurs an even number of times
unordered_map<ull, int> seen; ll res = 0;
for (int i = 0; i <= n; i++) res += seen[h.prefXor[i]]++;
return res;
}
// Subarrays where every value occurs a multiple of k times: hash sum_v (count_v mod k) * w_v modulo 2^61 - 1.
ll countMultipleOfKSubarrays(const vector<int>& a, int k, int maxValue, ull seed) {
const ull M = (1ULL << 61) - 1;
auto mulmod = [&](ull x, ull y) { __uint128_t z = (__uint128_t)x * y; ull r = (ull)(z & M) + (ull)(z >> 61); return r >= M ? r - M : r; };
mt19937_64 gen(seed); vector<ull> w(maxValue + 1); for (auto& x : w) x = gen() % M;
vector<int> cnt(maxValue + 1, 0); ull H = 0; map<ull, int> seen; seen[0] = 1; ll res = 0;
for (int v : a) {
if (cnt[v] == k - 1) { H = (H + M - mulmod(w[v], k - 1)) % M; cnt[v] = 0; } // digit k-1 -> 0
else { H = (H + w[v]) % M; cnt[v]++; }
res += seen[H]++;
}
return res;
}Proof: collision probabilities of set hashing
Two different multisets (as count vectors) collide iff . Pick a value where has the fewest factors of 2, say with (counts are below ). Fixing all other weights, the condition becomes , which has at most solutions among the values of ; when the multisets differ in an odd count somewhere the probability is , and in general it is at most (tiny for realistic counts). For the XOR hash, the sets of odd-count values differ in some , and uniform makes the XOR uniform: probability exactly . Even counts: every count in is even iff the prefix XOR hashes at and agree (with that error), so counting pairs of equal prefix values counts such subarrays. Multiples of : the prefix state is the vector of counts mod ; the hash is a linear function of that vector with random coefficients mod a prime , so two different states collide with probability (Schwartz–Zippel with degree 1). The update keeps . All functions were checked against brute force on random arrays with small values.
Freivalds and Schwartz–Zippel
// Probabilistic check of A * B == C (n x n, entries mod p), O(rounds * n^2); false is always correct.
bool freivalds(const vector<vector<ll>>& A, const vector<vector<ll>>& B, const vector<vector<ll>>& C, ll p, int rounds, mt19937_64& rng) {
int n = sz(A);
auto mul = [&](const vector<vector<ll>>& M, const vector<ll>& x) {
vector<ll> y(n, 0); for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) y[i] = (y[i] + (i128)M[i][j] * x[j]) % p; return y;
};
while (rounds--) {
vector<ll> r(n); for (auto& x : r) x = rng() % p;
if (mul(A, mul(B, r)) != mul(C, r)) return false;
}
return true;
}Schwartz–Zippel. A non-zero polynomial of total degree over a field, evaluated at a point whose coordinates are independent uniform elements of a finite set , vanishes with probability at most . This one lemma underlies polynomial identity testing (evaluate both sides of a conjectured identity at random points mod a large prime), string and tree hashing, the Tutte matching test and Freivalds with random vectors.
Proof: Schwartz–Zippel and Freivalds
Schwartz–Zippel. Induction on the number of variables . For a non-zero polynomial of degree has at most roots. For write with , . By induction ; if at the chosen point, is a non-zero polynomial of degree in and vanishes with probability . Union: .
Freivalds. If , then has a non-zero row , and is a non-zero linear polynomial in ; with uniform mod it vanishes with probability (with the bound is : pair each with the one that flips a coordinate where ; at most one of the pair gives 0). If the test always passes, so the error is one-sided. Checked: correct products always pass; products with one corrupted entry are always rejected in the tests.
Anti-hash attacks and defences
| Attack | Breaks |
|---|---|
| Thue–Morse strings of length | every polynomial hash modulo (natural ull overflow), for every base |
| even base modulo | only the last 64 characters matter: |
| birthday attack | a fixed in offline work: trivial for |
| keys that are multiples of the bucket count | unordered_map with the default hash<long long> (the identity on GCC) |
Defences: a random base modulo the Mersenne prime (or two independent moduli), seeded from the clock; for hash maps the randomized splitmix64 SafeHash in C++ for CP (or gp_hash_table with it); store lengths when sequences of different sizes must be distinguished; avoid zero encodings (map characters to ).
Proof: the Thue–Morse collision
Let “a”, (complement swaps a and b), and compare with . The difference of their hashes is evaluated at the base : by induction, the signed indicator of is . For odd , is divisible by (since for , and ), so the product is divisible by , which is for : strings of length 2048 collide for every odd base. For even bases , so characters at distance from the end are ignored. Checked with random bases. Lesson: never hash modulo alone on hackable problems.
Working rule. Find collisions in a tiny modulus deliberately to see the birthday bound in action; then quantify why a larger, independently randomized field changes the bound ( comparisons, error , plus when all pairs are compared).
Related notes
- Hashing (polynomial string hashing, double hashing)
- Randomized Algorithms
- Containers and Hashing