哈希重温

重写一下。高清重制版。

 

字符串哈希(进制哈希/一维哈希)


详解


把字符串通过哈希函数映射成一个哈希值,理想情况下,哈希值相等,字符串也就相等。

 

下文我们约束字符串为 $str$,长度分别为 $n$,以 $1$ 为起始下标。

类比成 $base$ 进制的理解,有:

$$hash(str)=\sum_{i=1}^{n} str_i \times base^{n-i}$$

利用前缀和的性质,设 $h_i$ 为字符串 $str$ 的前 $i$ 位的哈希值,这样可以高效表示哈希值。

不难得出:$h_i=h_{i-1} \times base + str_i$。可以类似秦九韶公式去理解。

 

于是,对于 $[L, R]$ 内的哈希值有: $h_R-h_{L-1} \times base^{R-L+1}$。

相当于把进制补齐,因为在算到 $h_R$ 时 $h_{L-1}$ 的哈希值共被累乘了 $base^{R-(L-1)}$。

而现在想得到 $[L, R]$ 内的哈希值,用 $h_R$ 的哈希值直接减去 $h_{L-1}$ 的哈希值时,需要先把 $h_{L-1}$ 的哈希值补回来。

 

如 $ABCDE - ABC$ 按照正常思维是 $DE$,但如果要取到 $DE$,我们要先补齐 $ABC$,变成 $ABC00$

然后再用 $ABCDE-ABC00=DE$,就像一个竖式,如下图

 

一些技巧:

1.通常,进制数 $base$ 取 $131$,$13331$ 等等,选这两个数冲突小到几乎无。

2.且需要对哈希值进行取模,数太大会爆。通常模数 $p$ 取一个大质数,不然容易冲突。

可以用自然溢出哈希,不用 $\mod p$,映射出来的数定义成 unsigned long long 类型,会自爆,自动取模。

3.用双哈希,防止被卡,双重保险(自然溢出容易被卡)

这里不同哈希函数,$base$ 不应相同。

 

例题


P3370 【模板】字符串哈希Code

板子,先把每个字符串映射成哈希值后,这样就能 $O(1)$ 比较了。

如何比较?直接排序就好了。

 

P2957 [USACO09OCT] Barn Echoes GCode

求一个字符串的前缀和另一个字符串的后缀相等的最长长度。板子。

 

 

二维哈希


详解


进制哈希就是一维的,二维也差不多,可以当成很个一维哈希拼在一起。也就是把若干行拼在一起,变成一行。

设 $h_{i,j}$ 表示矩阵的左上角为 $(1,1)$,右下角为 $(i,j)$ 时的哈希值。

类比二维前缀和,也是把进制补齐的方法,可以得到:$h_{i, j}=h_{i-1,j} \times base1 + h_{i,j-1} \times base2 - h_{i-1, j-1} \times base \times base2 + str_{i, j}$。

为了避免冲突,所以要用另一个 $base2$,因为行列的偏移量是不同的。这里对于行是 $base1$,列是 $base2$。

 

当然可以类似高维前缀和的方法,把行列分开单独处理,更好理解。

先从列开始 $h_{i,j}=h_{i,j-1} \times base2 + a_{i,j}$,再从行开始,则此刻转移为 $h_{i,j}=h_{i-1,j} \times base1 + h_{i,j}$。

 

获取 $(ax, ay)$ 到 $(bx, by)$ 的哈希值也是同理,进制补齐就好:

$$h_{bx, by} - h_{bx, ay-1} \times base2^{by-ay+1} - h_{ax-1, by} \times base1^{bx-ax+1} + h_{ax-1, ay-1} \times base1^{bx-ax+1} \times base2^{by-ay+1}$$

 

这里进制补齐的正确性是好证明的,可以把尝试把子矩阵的哈希值用 $a_{i, j}$ 逐个表示出来,然后再类比一维哈希补回来多累乘的值,你发现是可以分配进去的,正确性就是对的。

例题


P10474 [ICPC-Beijing 2011] Matrix 矩阵哈希

求小矩阵是否在大矩阵中出现过。小矩阵和大矩阵的大小固定。

 

因为大小都是固定的,可以直接预处理。

具体的就是在处理大矩阵 hash 值。在大矩阵中求每个小矩阵的 hash 值,用 map 记录这个值。

再算小矩阵 hash 值,查 map 中是否出现过即可。

 

就很板了,贴个代码:

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int N=1005;
const ull base=131, base2=13331;

char s[N][N];
ull p[N], p2[N];
int n, m, a, b, G;
struct mar
{
	int n, m, a[N][N];
	ull h[N][N];
	void init()
	{
		for (int i=1; i<=n; i++) for (int j=1; j<=m; j++) h[i][j]=h[i-1][j]*base+h[i][j-1]*base2-h[i-1][j-1]*base*base2+(ull)a[i][j];
	}
	ull get(int ax, int ay, int bx, int by)
	{
		return h[bx][by]-h[bx][ay-1]*p2[by-ay+1]-h[ax-1][by]*p[bx-ax+1]+h[ax-1][ay-1]*p[bx-ax+1]*p2[by-ay+1];
	}
}A, B;
map<ull, bool> mp;
int main()
{
	p[0]=p2[0]=1;
	for (int i=1; i<=1000; i++) p[i]=p[i-1]*base, p2[i]=p2[i-1]*base2;
	
	scanf("%d%d%d%d", &n, &m, &a, &b), A.n=n, A.m=m, B.n=a, B.m=b;
	for (int i=1; i<=n; i++) scanf("%s", s[i]+1);
	for (int i=1; i<=n; i++) for (int j=1; j<=m; j++) A.a[i][j]=s[i][j]-'0';
	
	A.init();
	for (int i=1; i+a-1<=n; i++) for (int j=1; j+b-1<=m; j++) mp[A.get(i, j, i+a-1, j+b-1)]=1;
	
	scanf("%d", &G);
	while (G--)
	{
		for (int i=1; i<=a; i++) scanf("%s", s[i]+1);
		for (int i=1; i<=a; i++) for (int j=1; j<=b; j++) B.a[i][j]=s[i][j]-'0';
		B.init();
		
		printf("%d\n", (int)mp[B.get(1, 1, a, b)]);
	}
	return 0;
}

  

 

树哈希


详解


树哈希是很灵活的,可以设计出各种各样的哈希方式,结合题目具体设计就好。

设节点 $u$ 的哈希值是 $h(u)$,我们当前先讨论不区分不同子树的情况。

因为不区分不同子树,可以用加法,就是可以累加子树的哈希(当然异或应该可以),即定为:

$$h(u)=G\left(\sum_{v\in\text{son}(u)}h(v)\right)$$

这里 $G$ 用于限制 $h$ 的大小,一般是取模。但是如果只有取模则所有哈希值均为零,所以可以改成加上一个常数然后取模。当然用自然溢出则不用考虑取模,加一个常数即可。

但显然,这太容易被卡了,考虑对每个哈希值进行一些变换,如下:

$$h(u)=G\left(\sum_{v\in\text{son}(u)}F(h(v))\right)$$

这里 $F$ 是某一个整数到整数的映射,可以用多项式混合异或+位移(学名叫 xor-shift) 的方法。

任何哈希都必然可以被对着卡掉,为了预防出题人对着 $xor-shift$ 卡,还可以在映射前后异或一个随机常数。这样就很难再被卡了。

具体的,$F$ 的写法如下(参考自 XorShift ):

mt19937_64 rnd(time(0));
ull rd=rnd();
ull merge(ull x)
{
	x^=rd, x^=(x<<5), x^=(x>>11), x^=(x<<54), x^=rd;
	return x;
}

 

如果需要区分不同子树呢?类似进制哈希的就好。把若干个子树的哈希值当成一维哈希里的数。

对于每一个 $u$ 的孩子 $v$,有:

$$h(u)=G\left( h(u) \times base + h(v)\right)$$

这种情况的写法就很多种了,可能比较难写出固定的不被卡的写法。

 

例题


UOJ 763. 树哈希

给出一个 $n$ 个点的有根树,你需要选择若干个子树,使得两两不同构,最大化选择子树数量。

有根树同构的板子。

同构:两棵树同构,当且仅当点数相同,且可以将一棵树重新标号,使得这两棵树完全相同。

 

这里同构是不考虑子树顺序的,于是按照套路设计哈希函数即可,然后求出所有子树的哈希值。

哈希值相同就是同构,那么用 set 维护不同哈希值的个数,就是不同同构的子树数量。

贴个代码:

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int N=1e6+5;

int n, u1, v1;
vector<int> a[N];
mt19937_64 rnd(time(0));
ull mark, mark2, h[N];
set<ull> s;
ull merge(ull x)
{
	x^=mark, x^=(x<<5), x^=(x>>11), x^=(x<<54), x^=mark;
	return x;
}
void dfs(int u, int fa)
{
	for (int i=0; i<a[u].size(); i++)
	{
		int v=a[u][i];
		if (v==fa) continue;
		dfs(v, u);
		
		h[u]+=merge(h[v]);
	}
	h[u]+=mark2, s.insert(h[u]);
}
int main()
{
	mark=rnd(), mark2=rnd();
	
	scanf("%d", &n);
	for (int i=1; i<n; i++)
	{
		scanf("%d%d", &u1, &v1);
		a[u1].push_back(v1);
		a[v1].push_back(u1);
	}
	dfs(1, 0);
	
	printf("%d", s.size());
	return 0;
}

  

P5043 【模板】树同构([BJOI2015]树的同构)

给定 $m$ 棵无根树,每棵树有一个编号。对于每一棵树,输出最小的与它重构的树的编号。

无根树同构板子。

考虑到我们构造的哈希函数是一个求和的形式,我们可以考虑换根 dp,求出以每一个点为根时的哈希值。

然后你可以钦定所有根哈希值中最大 / 最小的为整棵树的哈希值(如果你愿意,中位数、平均值也行)。

然后用 map 去维护就好了。

 

另外一种方法是找重心,只需要考虑重心为根的情况就好。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int N=1e6+5;

int G, n, u1;
vector<int> a[N];
mt19937_64 rnd(time(0));
ull mark, mark2, h[N], h2[N], mx=0;
map<ull, int> ans;
ull merge(ull x)
{
	x^=mark, x^=(x<<11), x^=(x>>45), x^=(x<<14), x^=mark;
	return x;
}
void dfs(int u, int fa)
{
	for (int i=0; i<a[u].size(); i++)
	{
		int v=a[u][i];
		if (v==fa) continue;
		dfs(v, u);
		
		h[u]+=merge(h[v]);
	}
	h[u]+=mark2;
}
void dfs2(int u, int fa)
{
	for (int i=0; i<a[u].size(); i++)
	{
		int v=a[u][i];
		if (v==fa) continue;
		
		h2[v]=h[v]+merge(h2[u]-merge(h[v]));
		dfs2(v, u);
	}
	h2[u]+=mark2, mx=max(mx, h2[u]);
}
int main()
{
	mark=rnd(), mark2=rnd();
	
	scanf("%d", &G);
	for (int T=1; T<=G; T++)
	{
		for (int i=1; i<=n; i++) a[i].clear(), h[i]=h2[i]=mx=0;
		
		scanf("%d", &n);
		for (int i=1; i<=n; i++)
		{
			scanf("%d", &u1);
			if (!u1) continue;
			a[i].push_back(u1);
			a[u1].push_back(i);
		}
		dfs(1, 0);
		h2[1]=h[1];
		dfs2(1, 0);
		
		if (ans[mx]) printf("%d\n", ans[mx]);
		else ans[mx]=T, printf("%d\n", T);
	}
	
	return 0;
}

  

 

P5018 [NOIP 2018 普及组] 对称二叉树

这题需要考虑子树的顺序。先鸽着吧。

 

不需要考虑子树顺序,咕咕咕:

SP7826 TREEISO - Tree Isomorphism

P4323 [JSOI2016] 独特的树叶

 

异或哈希


详解


异或哈希利用了异或操作的特殊性和哈希降低冲突的原理,处理一类 ”一个序列的排列组合是否在另一个序列出现“ 的问题。

因为是排列组合,是不考虑顺序的,刚好可以用上异或,但是单纯用异或冲突概率极大,便用上哈希降低冲突。

 

具体的,思考这样一个问题:给定长度为 $n$ 的序列 $a$ 和 $b$,判断能否让序列 $a$ 的某个排列等于序列 $b$。

发现是不关心顺序的,恰好异或有交换律,于是判断 $a_1 \oplus a_2 \oplus ... \oplus a_n = b_1 \oplus b_2 \oplus ... \oplus b_n$ 是否成立即可。

但显然这样是错的,因为不同数异或结果容易冲突。考虑用哈希把每个数映射成一个很大的数,这样冲突概率就很小了。

也就是考虑构造好了映射函数 $f$,然后就变成判断 $f(a_1) \oplus f(a_2) \oplus ...  \oplus f(a_n) = f(b_1) \oplus f(b_2) \oplus ...  \oplus f(b_n)$ 是否成立即可。 

 

例题


CF1175F The Number of Subpermutations

先把原 $a$ 序列用哈希映射成大数,这样就可以 $O(1)$ 判断一个区间的排列是否为 $1$ 到 $r-l+1$ 了。

暴力枚举区间还是 $O(n^2)$ 的,考虑观察性质降低复杂度。

 

观察到:合法区间一定包含 $1$,且其最大值就是区间长度。

于是从 $1$ 的位置左右扩展即可。复杂度降至 $O(n)$

 

参考


1.树哈希 Tree Hash

2.oi-wiki 树哈希

 

posted @ 2025-10-30 15:46  cn是大帅哥886  阅读(3)  评论(0)    收藏  举报