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

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;
}
posted @ 2023-11-14 21:25  RainPPR  阅读(33)  评论(0编辑  收藏  举报