字符串——哈希和康托展开 学习笔记
字符串——哈希和康托展开 学习笔记
哈希
原理就是通过哈希函数 \(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)删除。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战