Page Top

基本技巧——哈希和康托展开 学习笔记

基本技巧——哈希和康托展开 学习笔记

哈希

原理就是通过哈希函数 \(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

子串哈希

数学原理,对于 \(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
posted @ 2023-11-14 21:25  RainPPR  阅读(18)  评论(0编辑  收藏  举报