[字符串学习笔记] 1. 字符串 Hash
1.1. 定义
定义函数 \(f(s)\) 将 \(s\) 以某种方式映射为整数 \(h\),\(f\) 被称为 Hash 函数,\(h\) 被称为 Hash 值。
在使用同样的 Hash 函数的情况下,\(s, t\) 的 Hash 值可以用来判断 \(s, t\) 是否相等。
1.2. 性质
对于任意字符串 \(s, t\),有
- 若 \(f(s) \neq f(t)\),一定有 \(s \neq t\)。
- 若 \(f(s) = f(t)\),大概率 情况下 \(s = t\),但如果 \(s \neq t\),称为 Hash 碰撞。
1.3. 实现
多项式 Hash 是最广泛运用,且实现简单的一种 Hash 方法。
对于字符串 \(s\),通常采用 \(f(s) = \left( \sum_{i = 0}^{|s| - 1} b^{|s| - i - 1} \cdot s[i] \right) \bmod P\)。如 \(|s| = 3\) 时,\(f(s) = b^2 s[0] + bs[1] + s[2]\)。
此时可以将 Hash 值理解为一个 \(b\) 进制数。\(P\) 最好选择一个大质数。
const int B = 131, P = 998244353;
int gethash(const string s) {
int h = 0;
for (char i : s.size())
h = ((long long)h * B + i) % P;
return h;
}
另外,可以根据 C++ 特性,使用 无符号 整型的 自然溢出,来实现取模。
对于使用 \(l\) 位无符号整型的情况,相当于自动对 \(2^l\) 取模。
typedef unsigned long long ull;
const ull B = 131;
ull gethash(const string s) {
ull h = 0;
for (char i : s)
h = h * B + i;
return h;
}
例题 P3370 字符串哈希
题解
模版题。对字符串进行 Hash 后存到 \(h\) 中,接着排序 \(h\) 并计算结果。
参考代码:
const int N = 1e4 + 10, B = 131;
int n, res = 1;
vector<unsigned long long> h;
string s;
signed main() {
cin >> n;
while (n--) {
cin >> s;
int hsh = 0;
for (char i : s)
hsh = hsh * B + i;
h.push_back(hsh);
}
sort(h.begin(), h.end());
for (int i = 0; i < h.size() - 1; i++)
if (h[i] != h[i + 1]) res++;
cout << res;
}
1.4. Hash 碰撞
假定 Hash 函数随机映射 \(n\) 个字符串到 \(P\) 种不同值中。
处理第 \(i + 1\) 个字符串时,不发生碰撞的概率为 \(\frac{P - i} P\)。根据乘法原理,所有字符串不发生碰撞的概率为 \(\prod_{i = 0}^{n - 1} \frac{P - i} P\)。
在随机数据下,若 \(n = 10^5\),\(P = 998244353\),不发生碰撞的概率只有约 \(6.7\%\)。而若使用 64 位整型自然溢出的办法(即 \(P = 2^{64}\)),就算 \(n = 10^7\) 也几乎不可能有碰撞。
1.5. 多重 Hash
多重 Hash 是针对数据范围过大时出现 Hash 碰撞 导致无法正确判断的情况。在使用随机模数的情况下,多重 Hash 也能有效地应对卡特定模数的数据。
一般情况下,两个模数完全足够。具体地,定义 \(b_1, b_2\) 以及模数 \(P_1, P_2\),设立两个 Hash 函数 \(f_1(s), f_2(s)\)。若字符串 \(s, t\) 相同,则 \(f_1(s) = f_1(t)\) 且 \(f_2(s) = f_2(t)\)。
多重 Hash 的碰撞几率极低。
1.6. 查询子串 Hash
字符串 Hash 算法的时间复杂度为 \(\Theta(|s|)\)。若涉及到 \(m\) 次子串匹配时,直接求 Hash 的总复杂度无法接受。
考虑使用 类前缀和算法 完成。涉及到查找子串 Hash 时,默认字符串的下标从 \(1\) 开始。
令 \(p(s, i) = f(s[1 \ldots i])\),根据原来 \(b\) 进制的定义,易证 \(f(s[l \ldots r]) = p(r) - p(l - 1) \cdot b^{r - l + 1}\)。
通过递推
可以使用前缀和的方法 \(\Theta(|s|)\) 处理 \(p\) 函数。\(b\) 的幂次同理。
例题 ABC284F ABCBAC
题解
此处定义 \(\operatorname{rev}(p)\) 为 \(p\) 的逆序串,且 \(m\) 为 \(|s|\),即 \(2n\)。
可以发现,\(s[1 \ldots i] + s[i + n + 1 \ldots m] = \operatorname{rev}(s[i + 1 \ldots i + n])\)。
使用两个 Hash 数组 \(l, r\),对于 \(1 \leq i \leq m\),使用前缀和的方法求出
- \(l[i] = f(s[1 \ldots i])\)
- \(r[i] = f(\operatorname{rev}(s[i \ldots m]))\)
枚举所有可能的 \(i\) 并判断即可。
参考代码:
using ull = unsigned __int128;
const int N = 2e6 + 10, B = 131;
int n;
char s[N];
ull b_exp[N], hsh_l[N], hsh_r[N];
ull getleft(int l, int r) {
return hsh_l[r] - hsh_l[l - 1] * b_exp[r - l + 1];
}
ull getright(int l, int r) {
return hsh_r[l] - hsh_r[r + 1] * b_exp[r - l + 1];
}
signed main() {
cin >> n >> (s + 1);
int m = n * 2;
b_exp[0] = 1;
for (int i = 1; i <= m; i++)
b_exp[i] = b_exp[i - 1] * B;
for (int i = 1; i <= m; i++)
hsh_l[i] = hsh_l[i - 1] * B + s[i];
for (int i = m; i >= 1; i--)
hsh_r[i] = hsh_r[i + 1] * B + s[i];
for (int i = 1; i <= n; i++) {
ull pre = getleft(1, i), rev = getright(i + 1, i + n), suf = getleft(i + n + 1, m);
if (pre * b_exp[n - i] + suf == rev) {
for (int j = i + n; j >= i + 1; j--)
cout << s[j];
cout << '\n'
<< i;
return 0;
}
}
cout << -1;
}
例题 CF113B Petr#
题面
给定字符串 \(s, a, b\),求 \(s\) 中有 \(a\) 这个前缀且有 \(b\) 这个后缀的子串个数。
题解
考虑子串 \(s[i \ldots j]\),需要满足两点要求:
- \(|s[i \ldots j]|\),即 \(j - i + 1\) 不小于 \(\max(|a|, |b|)\);
- 前缀 \(s[i \ldots i + |a| - 1] = a\),后缀 \(s[j - |b| + 1 \ldots j] = b\)。
枚举所有的 \((i, j)\) 并 Hash 判断即可。最坏可能下,时间复杂度 \(\Theta({|s|}^2 \log {|s|}^2)\)。同理,可以使用 STL 的 unordered_map Hash 表进一步提升效率至 \(\Theta({|s|}^2)\)。
参考代码:
using ull = unsigned long long;
const int N = 2010, B = 131;
int res;
char s[N], a[N], b[N];
ull b_exp[N], hsh_s[N], hsh_a, hsh_b;
vector<ull> h;
ull gethash(int l, int r) {
return hsh_s[r] - hsh_s[l - 1] * b_exp[r - l + 1];
}
signed main() {
cin >> (s + 1) >> (a + 1) >> (b + 1);
int n = strlen(s + 1), p = strlen(a + 1), q = strlen(b + 1);
b_exp[0] = 1;
for (int i = 1; i <= n; i++) {
b_exp[i] = b_exp[i - 1] * B;
hsh_s[i] = hsh_s[i - 1] * B + s[i];
}
for (int i = 1; i <= p; i++)
hsh_a = hsh_a * B + a[i];
for (int i = 1; i <= q; i++)
hsh_b = hsh_b * B + b[i];
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
if (j - i + 1 >= max(p, q))
if (gethash(i, i + p - 1) == hsh_a && gethash(j - q + 1, j) == hsh_b)
h.push_back(gethash(i, j));
sort(h.begin(), h.end());
cout << unique(h.begin(), h.end()) - h.begin();
}
例题 CF985F Isomorphic Strings
题解
记录一个字符串中 每种 字符 \(c\) 出现的位置,那么若两个字符串同构,虽然字符不同,但出现的位置 必定重复。
具体地,对于 每种 字符 \(c\),遍历原串 \(s\),某一位 \(= c\) 时记录为 \(\texttt 1\),否则记录为 \(\texttt 0\),得到一个 01 串并对其进行 Hash。每次询问时,计算出两个子串各 \(26\) 个 Hash 值,存储并排序。若排序后结果相等,则说明两个子串同构。
时间复杂度 \(O(|s|)\)。参考代码:
const int N = 2e5 + 10, B = 131, P = 1234567891;
int n, m, x, y, k;
long long b_exp[N], hsh[26][N];
char s[N];
int gethash(int c, int l, int r) {
return (hsh[c][r] - hsh[c][l - 1] * b_exp[r - l + 1] % P + P) % P;
}
signed main() {
cin >> n >> m >> (s + 1);
b_exp[0] = 1;
for (int i = 1; i <= n; i++)
b_exp[i] = b_exp[i - 1] * B % P;
for (int i = 0; i < 26; i++)
for (int j = 1; j <= n; j++)
hsh[i][j] = (hsh[i][j - 1] * B + (s[j] - 'a' == i)) % P;
while (m--) {
cin >> x >> y >> k;
vector<long long> vecx, vecy;
for (int i = 0; i < 26; i++)
vecx.push_back(gethash(i, x, x + k - 1));
sort(vecx.begin(), vecx.end());
for (int i = 0; i < 26; i++)
vecy.push_back(gethash(i, y, y + k - 1));
sort(vecy.begin(), vecy.end());
if (vecx == vecy)
cout << "YES\n";
else
cout << "NO\n";
}
}
1.7. 字符串匹配
先求出模式串 \(t\) 的 Hash 值 \(f(t)\)。求出待匹配串 \(s\) 中所有长度为 \(|t|\) 的子串,逐一比较即可。
例题 P8630 密文搜索
题解
可以注意到,如果字符串 \(t\) 可以重排为 \(s\),那么 \(s, t\) 排序后必然相同。
直接 Hash 并遍历 \(s\) 长度为 \(8\) 的所有子串查找即可。由于长度只有 \(8\),无需使用 查询子串 Hash 中所介绍的方法,直接暴力 Hash 即可。
时间复杂度 \(\Theta(n |s|)\)。可以使用 STL 的 unordered_map Hash 表进一步提升效率至 \(\Theta(|s|)\)。
参考代码:
const int B = 131;
string s, t;
int n, res;
vector<unsigned long long> h;
signed main() {
cin >> s >> n;
if (s.size() < 8) {
cout << 0;
return 0;
}
while (n--) {
cin >> t;
sort(t.begin(), t.end());
int hsh = 0;
for (char i : t)
hsh = hsh * B + i;
h.push_back(hsh);
}
for (int i = 0; i < s.size() - 7; i++) {
string p = s.substr(i, 8);
sort(p.begin(), p.end());
int hsh = 0;
for (char i : p)
hsh = hsh * B + i;
for (int j : h)
if (j == hsh) res++;
}
cout << res;
}
习题
参见 字符串 Hash 习题。
- P10467 [CCC 2007] Snowflake Snow Snowflakes
- P10468 兔子与兔子
- P6739 [BalticOI 2014 Day1] Three Friends
- CF1979D Fixing a Binary String
- CF835D Palindromic characteristics
- CF7D Palindrome Degree
- CF1326D1 Prefix-Suffix Palindrome (Easy version)
- CF1109B Sasha and One More Name
- P4824 [USACO15FEB] Censoring S
- CF39J Spelling Check
- CF727E Games on a CD
- P3449 [POI2006] PAL-Palindromes

浙公网安备 33010602011771号