Definition
We define a function that maps strings to integers; this is called a hash function.
We hope that this function can conveniently help us determine whether two strings are equal.
The idea of hashing
The core idea of hashing is to map the input into a relatively small range whose values are convenient to compare.
Warning
The meaning of “a relatively small value range” differs in different situations.
In a hash table, the value range must be small enough to allow linear space and time complexity.
In string hashing, the value range must be small enough for fast comparison ( and can both be compared quickly).
At the same time, the value range cannot be too small, in order to reduce the collision rate.
Properties
More specifically, the most important properties of a hash function can be summarized as follows:
-
If the hash values are different, the two strings must be different;
-
If the hash values are the same, the two strings are not necessarily the same (although they are likely to be the same, and of course we would like them to always be the same).
We call the phenomenon in which two different original strings have the same hash value a hash collision.
Explanation
What do we need to pay attention to?
Time complexity and hash accuracy.
We usually use polynomial hashing. For a string of length , we can define the polynomial hash function as . For example, for the string , its hash value is .
It is worth noting that many people also use another definition of the hash function, namely . Under this definition, the hash value of the same string becomes .
Clearly, both of the above hash-function definitions are valid, but the formulas used to calculate substring hash values later are different for the two definitions. Therefore, be very careful not to confuse these two different hashing methods.
Because the first hash definition is easier to calculate, is more widely used, and can be understood by analogy with a base- number, all hash functions discussed below in this article use as their definition.
Also, for convenience and to use a larger modulus, in C++ we sometimes use unsigned long long to define the result of the hash function. Due to C++'s behavior, this is equivalent to setting the modulus to , which is also a good choice.
Accuracy will be discussed later.
Hash collision probability analysis
Hash collisions
A hash collision means that two different strings map to the same hash value.
Let the hash value space (the number of all possible strings) be , and let the number of computations (the number of strings to be hashed) be .
Then the probability of a hash collision is:
Proof
When every hash value is generated with the same probability, the probability of no hash collision is:
Simplifying gives:
Therefore, the probability of a hash collision is:
This formula is still too complicated, so let us simplify it further.
By Taylor's formula:
When is extremely small, approaches .
Substituting this into the original formula for no hash collision:
Simplifying:
Therefore, the probability of a hash collision is:
Breaking large-modulus hashes
Observe the formula:
To break a hash, we need to satisfy the following conditions:
- should be greater than the modulus.
- should be as small as possible.
For example:
If the character set consists of uppercase and lowercase letters and digits, with modulus :
Thus, for this range, if we randomly generate strings of length , the probability that they have the same hash value is as high as .
Breaking natural-overflow hashes
This hash has a modulus that is too large to be broken by the method above, so we need another method.
First, this hash has the form , so we discuss cases according to .
Even b
Here , where is .
It is easy to see that if , then .
Therefore, we only need to construct strings of the form:
aaa...a
baa...a
with length greater than to obtain a collision.
Odd b
Define as the string obtained by flipping every character in .
Example:
That is, turn a into b, and turn b into a.
Now define as the hash value of , and as the hash value of .
Repeatedly construct .
and are the two strings we need.
Derivation
First, we have:
Try subtracting:
We see a , but the original expression is too complicated, so let us change variables:
Let:
From the original expression:
Since is always odd, is always even.
Therefore:
But this is too weak: we need to break the hash. Let us simplify further:
Thus has the form .
Therefore , , and so on; that is,
Thus, when , we can make , which meets the requirement.
Example problems
Example: BZOJ 3097 Hash Killer I
Given a hash implemented with natural overflow, construct a string that breaks it.
Example: BZOJ 3097 Hash Killer II
Given a hash implemented with a large modulus, construct a string that breaks it.
Example: Luogu U461211 String Hash (strengthened data)
Given strings, determine how many of them are different.
Improving Hashing
Multiple-value hashing
After seeing so many ways to break hashes above, there are of course ways to solve the problem as well.
Multiple-value hashing uses multiple hash functions, each with a different modulus, which solves the hash-collision problem.
When comparing, if even one of the hash values differs, regard the two strings as different; if all hash values are the same, regard the two strings as the same.
In general, double hashing is sufficient.
Hashing substrings for multiple queries
Computing the hash of one string once has complexity , where is the string length, so it is no different from brute-force matching. If we need to query substring hashes of one string many times, recomputing each time is very inefficient.
The usual method is to precompute the hash value of every prefix of the entire string. Regard the hash value as the result of taking a base- number modulo , so that each substring hash can then be found quickly:
Let denote , namely the hash value of the prefix of the original string of length . By definition,
Now we want to find quickly in a way similar to prefix sums. By definition, the hash value of the string is
Comparing the two expressions above, we find that holds (you can substitute manually to verify it), so this expression gives the substring hash quickly. The value can be precomputed in and then each query can be answered in (or each query can be answered with fast exponentiation in ).
Implementation
Modular hash
Note: it is less efficient and not recommended in practice.
C++
using std::string;
constexpr int M = 1e9 + 7;
constexpr int B = 233;
using ll = long long;
int get_hash(const string& s) {
int res = 0;
for (int i = 0; i < s.size(); ++i) {
res = ((ll)res * B + s[i]) % M;
}
return res;
}
bool cmp(const string& s, const string& t) {
return get_hash(s) == get_hash(t);
}
Python
M = int(1e9 + 7)
B = 233
def get_hash(s):
res = 0
for char in s:
res = (res * B + ord(char)) % M
return res
def cmp(s, t):
return get_hash(s) == get_hash(t)
Double hash
C++
using ull = unsigned long long;
ull base = 131;
ull mod1 = 212370440130137957, mod2 = 1e9 + 7;
ull get_hash1(std::string s) {
int len = s.size();
ull ans = 0;
for (int i = 0; i < len; i++) ans = (ans * base + (ull)s[i]) % mod1;
return ans;
}
ull get_hash2(std::string s) {
int len = s.size();
ull ans = 0;
for (int i = 0; i < len; i++) ans = (ans * base + (ull)s[i]) % mod2;
return ans;
}
bool cmp(const std::string s, const std::string t) {
bool f1 = get_hash1(s) != get_hash1(t);
bool f2 = get_hash2(s) != get_hash2(t);
return f1 || f2;
}
Python
def get_hash1(s: str) -> int:
base = 131
mod1 = 212370440130137957
ans = 0
for char in s:
ans = (ans * base + ord(char)) % mod1
return ans
def get_hash2(s: str) -> int:
base = 131
mod2 = 1000000007
ans = 0
for char in s:
ans = (ans * base + ord(char)) % mod2
return ans
def cmp(s: str, t: str) -> bool:
f1 = get_hash1(s) != get_hash1(t)
f2 = get_hash2(s) != get_hash2(t)
return f1 or f2
Applications of Hashing
String matching
After calculating the hash value of the pattern, calculate the hash values of every substring of the text whose length equals the pattern length, and compare each with the pattern's hash value.
String matching with up to mismatches
Problem: Given a source string of length and a pattern string of length , find how many substrings of the source string match the pattern. matches if and only if and have the same length and differ at no more than positions. Here and .
This problem cannot be solved with KMP, but it can be solved with hashing plus binary search.
Enumerate all possible matching substrings. Suppose the currently enumerated substring is . Hashing plus binary search can quickly find the first position at which and differ. Then delete from and the mismatching position and all positions before it, and continue searching for the next mismatch. This process occurs at most times.
The total time complexity is .
Longest palindromic substring
Binary-search the answer. When checking feasibility, enumerate the palindrome centers (axes of symmetry) and use hashes to determine whether the two sides are equal. The hashes in forward and reverse order need to be precomputed separately. The time complexity is .
This problem can be solved by the Manacher algorithm in time.
Hashing can also solve this problem in . Specifically, let denote the length of the longest palindrome ending at ; then the answer is . Since , we only need to brute-force downward from until we find the first palindrome. Let denote the current enumerated , initially . Whenever increases, increases by , and each brute-force loop then decreases it by . Thus, the brute-force loop occurs at most times, and the total time complexity is .
Longest common substring
Problem: Given nonempty strings with total length at most , find the longest common substring of all strings; if there are multiple, output any one. Here .
Clearly, if a longest common substring of length exists, then a common substring of length must also exist. Therefore, we can binary-search the length of the longest common substring. Suppose the current length is . The logic of check(k) is to hash all substrings of length in every string and store the hash values in hash tables. Then take their intersection.
The time complexity is .
Determining the number of distinct substrings in a string
Problem: Given a string of length consisting only of lowercase English letters, find the number of distinct substrings in the string.
To solve this problem, we enumerate all substrings of each length . For each length , we multiply its hash value by the same power of and store it in an array. The number of distinct elements in the array equals the number of distinct substrings of that length, and this number is added to the final answer.
For convenience, we use as the prefix characters of the hash and define .
Reference code
int count_unique_substrings(string const& s) {
int n = s.size();
constexpr static int b = 31;
constexpr static int m = 1e9 + 9;
vector<long long> b_pow(n);
b_pow[0] = 1;
for (int i = 1; i < n; i++) b_pow[i] = (b_pow[i - 1] * b) % m;
vector<long long> h(n + 1, 0);
for (int i = 0; i < n; i++)
h[i + 1] = (h[i] + (s[i] - 'a' + 1) * b_pow[i]) % m;
int cnt = 0;
for (int l = 1; l <= n; l++) {
set<long long> hs;
for (int i = 0; i <= n - l; i++) {
long long cur_h = (h[i + l] + m - h[i]) % m;
cur_h = (cur_h * b_pow[n - i - 1]) % m;
hs.insert(cur_h);
}
cnt += hs.size();
}
return cnt;
}
Example problems
CF1200E Compress Words
Given several strings, the answer string is initially empty. At step , append the -th string to the end of the answer string, removing as much duplicate content as possible (that is, remove the longest string that is both a suffix of the current answer string and a prefix of the -th string). Find the final string.
The number of strings is at most , and the total length is at most .
Solution
Each time, we need to find the longest string that is both a suffix of the current answer string and a prefix of the -th string. Enumerate its length and compare hashes.
This problem can of course also be solved with the KMP algorithm.
Reference code
#include <cassert>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
constexpr int L = 1e6 + 5;
constexpr int HASH_CNT = 2;
int hashBase[HASH_CNT] = {29, 31};
int hashMod[HASH_CNT] = {int(1e9 + 9), 998244353};
struct StringWithHash {
char s[L];
int ls;
int hsh[HASH_CNT][L];
int pwMod[HASH_CNT][L];
void init() { // Initialize
ls = 0;
for (int i = 0; i < HASH_CNT; ++i) {
hsh[i][0] = 0;
pwMod[i][0] = 1;
}
}
StringWithHash() { init(); }
void extend(char c) {
s[++ls] = c; // Record the character count and each character
for (int i = 0; i < HASH_CNT; ++i) { // Precompute the double hash
pwMod[i][ls] =
1ll * pwMod[i][ls - 1] * hashBase[i] % hashMod[i]; // Obtain b^ls
hsh[i][ls] = (1ll * hsh[i][ls - 1] * hashBase[i] + c) % hashMod[i];
}
}
vector<int> getHash(int l, int r) { // Obtain the hash value
vector<int> res(HASH_CNT, 0);
for (int i = 0; i < HASH_CNT; ++i) {
int t =
(hsh[i][r] - 1ll * hsh[i][l - 1] * pwMod[i][r - l + 1]) % hashMod[i];
t = (t + hashMod[i]) % hashMod[i];
res[i] = t;
}
return res;
}
};
bool equal(const vector<int> &h1, const vector<int> &h2) {
assert(h1.size() == h2.size());
for (unsigned i = 0; i < h1.size(); i++)
if (h1[i] != h2[i]) return false;
return true;
}
int n;
StringWithHash s, t;
char str[L];
void work() {
int len = strlen(str); // Get the string length
t.init();
for (int j = 0; j < len; ++j) t.extend(str[j]);
int d = 0;
for (int j = min(len, s.ls); j >= 1; --j) {
if (equal(t.getHash(1, j), s.getHash(s.ls - j + 1, s.ls))) { // Compare hash values
d = j;
break;
}
}
for (int j = d; j < len; ++j) s.extend(str[j]); // Update the answer string
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> str;
work();
}
cout << s.s + 1 << '\n';
return 0;
}
This page is partially translated from the blog String hash and its English translation String Hashing. The Russian version is licensed under Public Domain + Leave a Link; the English version is licensed under CC-BY-SA 4.0.