字符串——哈希和康托展开 学习笔记
字符串——哈希和康托展开 学习笔记
哈希
原理就是通过哈希函数 \(h(\mathit{key})\) 将 \(\mathit{key}\) 映射为一个数,方便储存,判断存在的。
因此任何一个哈希函数,除了 \(h(x)=x\) 这样的,都会存在冲突的情况,即对于 \(i \neq j,h(i)=h(j)\) 存在。
解决这种东西的方法有「拉链法」「开放寻址法」「二次哈希」三种:
- 拉链法:对于映射出的每个位置,建一个链表,访问的时候依次遍历这个位置上的链表;
- 开放寻址法:如果这个位置被占用了,就往后一个一个找,直到找到为止;
- 二次哈希:定义 \(f(x)\),如果有冲突 \(f(x):=f(f(x))\)。
可以使用 unordered_map 这种 STL 自带的哈希。
如果是 Codeforces 会 Hack 的,用 pb_ds 就行。
不会有人想手写哈希吧?真的没有必要吧(起码我现在的水平来说。
字符串哈希
常见的是 BKDR-Hash 算法,简要说就是把字符串看成一个 \(\mathit{base}\)(底数)进制数,然后对一个模数 \(\mathit{mod}\) 取模。
这样也会有冲突的存在,如何解决?
错误率:
假定哈希函数将字符串随机地映射到大小为 \(M\) 的值域中,总共有 \(n\) 个不同的字符串,那么未出现碰撞的概率是 \(\prod_{i=0}^{n-1}\frac{M-i}{M}\)(第 \(i\) 次进行哈希时,有 \(\frac{M-i}{M}\) 的概率不会发生碰撞)。在随机数据下,若 \(M=10^9+7\),\(n=10^6\),未出现碰撞的概率是极低的。
所以,进行字符串哈希时,经常会对两个大质数分别取模,这样的话哈希函数的值域就能扩大到两者之积,错误率就非常小了。
- 合理选择模数:理论上(根据生日悖论?)模数最好大于数据规模且是质数,常用的模数有 \(998244353\)、\(10^9+7\)、\(10^9+9\);
- 合理选择底数:底数需要取到字符串最大取值以上的一个质数,如果字符串每一位映射到了 \(0\sim25/9\) 就可以用 \(53\)、\(97\) 一类,否则可以随便选一个 \(200\) 以上的质数;
- 双哈希:选择两对模数、底数 \((b_1,m_1),(b_2,m_2)\) 分别计算,但常数较大,理论上 CCF 不卡哈希。
实现:
function bkdr_hash(str, base, mod) -> hash_type:
h := 0
for c in str:
h := h * base + c
h := h % mod
return h
UPD:现在一般选 \(\langle131,e9+7\rangle;\langle13331,1e9+9\rangle\),因为后者是孪生素数。
子串哈希
数学原理,对于 \(p\) 进制数,可以 \(\mathcal{O}(1)\) 的取出其某几位,即一个子串的哈希值。
此时我们就需要求出该字符串任意前缀的哈希值 \(h_k(s)\),同时预处理出 \(b_i=p^i\),加速求解。
实现:
function bkdr_hash2(str, base, mod) -> hash_type[0..len(str)]{}:
n := len(str)
h[0..n] := {0}
b[0..n] := {0}
for i in [0, n):
h[i] = (h[i - 1] * base + str[i]) % mod
b[i] = (b[i - 1] * base) % mod // 注意不要忘了 mod
return {h, b}
function substr_hash(str, {h, b}[0..n], [l, r], base, mod) -> hash_type:
if l <> 0:
return (h[r] - h[l - 1] * b[r - l + 1] % mod + mod) % mod
else:
return h[r] // 此时 l - 1 == -1
康托展开
用途:将 \(1 \sim n\) 的排列映射为其字典序排名 \(\mathit{rk}\)。
时间复杂度:\(\mathcal{O}(n^2)\),树状数组、线段树可以优化到 \(\mathcal{O}(n \log n)\)。
其根本原理是,根据字典序的定义,只要前面的数字小,那么字典序一定小,与后面无关。
举例说明:对于长为 \(5\) 的排列 \([2,5,3,4,1]\):它大于以 \(1\) 为第一位的任何排列,以 \(1\) 为第一位的 \(5\) 的排列有 \(4!\) 种。这是非常好理解的。对第二位的 \(5\) 而言,它大于第一位与这个排列相同的,而这一位比 \(5\) 小的所有排列。不过我们要注意的是,这一位不仅要比 \(5\) 小,还要满足没有在当前排列的前面出现过,不然统计就重复了。因此这一位为 \(1,3\) 或 \(4\) ,第一位为 \(2\) 的所有排列都比它要小,数量为 \(3\times3!\)。按照这样统计下去,答案就是 \(1+4!+3\times3!+2!+1=46\)。注意我们统计的是排名,因此最前面要 \(+1\) 。
注意到我们每次要用到当前有多少个小于它的数还没有出现,这里用树状数组统计比它小的数出现过的次数就可以了。
实现:
function cantor(n, a[1..n]) -> Integer:
for i in [1, n], fac[i] := i!
ans := 0
for i in [1, n]:
cnt := 0
for j in [i + 1, n]:
cnt += [a[j] < a[i]]
ans += cnt * fac[n - i]
return ans
STL
手写 set、map 的判定的时候,
struct cmp_y {
bool operator ()(const emm &a, const emm &b) const {
return a.y < b.y;
}
};
后面的 const 不可省略。
哈希函数同理,
struct my_hash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t FIXED_RANDOM =
chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + FIXED_RANDOM);
}
size_t operator()(pair<uint64_t, uint64_t> x) const {
static const uint64_t FIXED_RANDOM =
chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x.first + FIXED_RANDOM) ^
(splitmix64(x.second + FIXED_RANDOM) >> 1);
}
};
二维哈希
对第一维前缀哈希,第二维选择不同的基数,对第一维哈希值进行哈希。
例题:P10474 [BeiJing2011] Matrix 矩阵哈希。
#include <bits/stdc++.h>
using namespace std;
using tp = unsigned long long;
constexpr tp P1 = 131, P2 = 13331;
constexpr int N = 1010;
int n, m, A, B;
int a[N][N];
tp f[N][N], g[N][N];
tp qpow(tp a, tp b) {
tp r = 1;
for (; b; b >>= 1) {
if (b & 1) r = r * a;
a = a * a;
}
return r;
}
int t[N][N];
tp tf[N][N], tg[N][N];
unordered_set<tp> app;
string s;
void solev() {
for (int i = 1; i <= A; ++i) { cin >> s; for (int j = 1; j <= B; ++j) t[i][j] = s[j - 1] - '0'; }
for (int i = 1; i <= A; ++i) for (int j = 1; j <= B; ++j) tf[i][j] = tf[i][j - 1] * P1 + t[i][j];
for (int j = 1; j <= B; ++j) for (int i = 1; i <= A; ++i) tg[i][j] = tg[i - 1][j] * P2 + tf[i][j];
cout << app.count(tg[A][B]) << endl;
}
signed main() {
ios::sync_with_stdio(false); cin.tie(nullptr), cout.tie(nullptr);
cin >> n >> m >> A >> B; tp frA = qpow(P2, A), frB = qpow(P1, B);
for (int i = 1; i <= n; ++i) { cin >> s; for (int j = 1; j <= m; ++j) a[i][j] = s[j - 1] - '0'; }
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) f[i][j] = f[i][j - 1] * P1 + a[i][j];
for (int j = 1; j <= m; ++j) for (int i = 1; i <= n; ++i) g[i][j] = g[i - 1][j] * P2 + f[i][j];
for (int i = A; i <= n; ++i) for (int j = B; j <= m; ++j) app.insert(g[i][j] - g[i - A][j] * frA - g[i][j - B] * frB + g[i - A][j - B] * frA * frB);
int T; cin >> T; while (T--) solev();
return 0;
}
本文来自博客园,作者:RainPPR,转载请注明原文链接:https://www.cnblogs.com/RainPPR/p/hash-cantor.html
如有侵权请联系我(或 2125773894@qq.com)删除。