[字符串总结] Hash KMP Trie树

哈希

用于比较两个字符串是否相等;

本质就是把一个字符串看成一个 $ base $ 进制的数( $ base $ 自定),每一位是这一位的字符对应的 $ ASCII $ 值,在比较时只需判断这两个数(即哈希值)是否相等即可;

一般的,$ base $ 会选一个质数( $ 200+ $ 即可),很容易发现,一个字符串的哈希值是很大的,所以要进行取模;

Hash冲突

当 $ Hash $ 值映射的范围很小时(如1e9 + 7),有可能出现两个不同的字符串 $ Hash $ 值相等的情况,这就是 $ Hash $ 冲突;

$ Hash $ 一般采用的打法有两种:

自然溢出Hash

我们都知道,当一个数超出了这个数的数据范围时会溢出,那么我们就可以利用这个特性,再求 $ Hash $ 值的时候使其溢出,又因为 $ Hash $ 值非负(根据定义),所以采用 $ unsigned $ 类型存储(一般为 $ unsigned \ long \ long $);

优点:代码简便;

缺点:容易被卡(这相当于使出题人知道了你的模数,可以构建特殊数据造成Hash冲突);

例题:给出字母表 $ {'A','B','C',...,'Z'} $ 和两个仅有字母表中字母组成的有限字符串:单词 $ W $ 和文章 $ T $,找到 $ W $ 在 $ T $ 中出现的次数。这里“出现”意味着 $ W $ 中所有的连续字符都必须对应 $ T $ 中的连续字符。$ T $ 中出现的两个 $ W $ 可能会部分重叠。

从左往右遍历 $ T $ 中每个长度为 $ |W| $ 的字串并和 $ W $ 逐个比较;

这里可以运用前缀和优化,具体看代码:

#include <iostream>
#include <string>
#include <cstring>
#include <cmath>
using namespace std;
const unsigned long long pep = 229;
int t;
string a, b;
unsigned long long h[10000005];
unsigned long long p[10000005];
unsigned long long get_hash(string x) {
	unsigned long long ans = 0;
	for (int i = 0; i < x.size(); i++) {
		ans = (ans * pep + x[i]); //暴力求Hash值,一位一位的加入(类比十进制);
	}
	return ans;
}
int main() {
	cin >> t;
	p[0] = 1; //p[i]代表进制pep的i次方;
	for (int i = 1; i <= 100005; i++) p[i] = (p[i - 1] * pep);
	while(t--) {
		int ans = 0;
		memset(h, 0, sizeof(h));
		cin >> a >> b;
		unsigned long long c = get_hash(a);
		int lena = a.size();
		h[1] = b[0];
		h[0] = 0;
		for (int i = 2; i <= b.size(); i++) {
			h[i] = (h[i - 1] * pep + b[i - 1]); //这里的h数组相当于一个前缀和,这样就可以每次 O(1)求出其子串的Hash值了;
		}
		for (int i = lena; i <= b.size(); i++) {
			unsigned long long d = 0;
			d = (h[i] - h[i - lena] * p[lena]); //这里可以类比十进制;
			if (d == c) ans++;
		}
		cout << ans << endl;
	}
	return 0;
}

双模数Hash

用单模数很容易造成 $ Hash $ 冲突,原因就在于 $ Hash $ 值的值域太小,所以我们可以考虑双模数 $ Hash $;

双模数 $ Hash $ 本质上就是用两个不同的模数计算两个字符串的 $ Hash $ 值,如果这两个 $ Hash $ 值分别相等,则这两个字符串相等;

这样值域就扩大到了两个模数相乘的范围,一般两个模数在 $ int $ 范围内即可;

当然,两个模数在 $ long \ long $ 范围内也行,只不过 $ long \ long $ * $ long \ long $ 需要用到快速乘,容易超时,所以一般不用;

好像模数需要是质数(貌似是因为 $ CRT $ ),但好像不是质数也行(这里参考别的博客吧);

例题:和上面一样;

#include <iostream>
#include <string>
#include <cstring>
#include <cmath>
using namespace std;
const long long mod1 = 99999885229;
const long long mod2 = 99999886229;
const long long pep = 229;
int t; //以下各数组和上面的意义一样;
string a, b;
long long h[10000005];
long long p[10000005];
long long pp[10000005];
long long hh[10000005];
long long ksc(long long a, long long b, long long pp) {  //快速乘;
	long long ans = 0;
	while(b) {
		if (b & 1) ans = (ans + a) % pp;
		b >>= 1;
		a = (a + a) % pp;
	}
	return ans;
}
long long get_hash(string x) {
	long long ans = 0;
	for (int i = 0; i < x.size(); i++) {
		ans = (ksc(ans, pep, mod1) + x[i] % mod1) % mod1;
	}
	return ans;
}
long long get_hash1(string x) {
	long long ans = 0;
	for (int i = 0; i < x.size(); i++) {
		ans = (ksc(ans, pep, mod2) + x[i] % mod2) % mod2;
	}
	return ans;
}
int main() {
	cin >> t;
	p[0] = 1;
	pp[0] = 1;
	for (int i = 1; i <= 100005; i++) p[i] = ksc(p[i - 1], pep, mod1);
	for (int i = 1; i <= 100005; i++) pp[i] = ksc(pp[i - 1], pep, mod2);
	while(t--) {
		int ans = 0;
		memset(h, 0, sizeof(h));
		memset(hh, 0, sizeof(hh));
		cin >> a >> b;
		long long c = get_hash(a);
		long long e = get_hash1(a);
		int lena = a.size();
		h[1] = b[0];
		h[0] = 0;
		hh[0] = 0;
		hh[1] = b[0];
		for (int i = 2; i <= b.size(); i++) {
			h[i] = (ksc(h[i - 1], pep, mod1) + b[i - 1] % mod1) % mod1;
		}
		for (int i = 2; i <= b.size(); i++) {
			hh[i] = (ksc(hh[i - 1], pep, mod2) + b[i - 1] % mod2) % mod2;
		}
		for (int i = lena; i <= b.size(); i++) {
			long long d = 0;
			long long d1 = 0;
			d = (h[i] % mod1 - ksc(h[i - lena], p[lena], mod1) + mod1) % mod1;
			d1 = (hh[i] % mod2 - ksc(hh[i - lena], pp[lena], mod2) + mod2) % mod2;
			if (d == c && d1 == e) ans++;
		}
		cout << ans << endl;
	}
	return 0;
}

在 $ |W| <= |T| <= 1000000 $ 的情况下,这个代码会超时(因为有快速乘),所以模数尽量开 $ int $ 内的;

#include <iostream>
#include <string>
#include <cstring>
#include <cmath>
using namespace std;
const long long mod1 = 1e9 + 7;
const long long mod2 = 998244353;
const long long pep = 229;
int t; //以下各数组和上面的意义一样;
string a, b;
long long h[10000005];
long long p[10000005];
long long pp[10000005];
long long hh[10000005];
long long ksc(long long a, long long b, long long pp) {  //快速乘;
	long long ans = 0;
	while(b) {
		if (b & 1) ans = (ans + a) % pp;
		b >>= 1;
		a = (a + a) % pp;
	}
	return ans;
}
long long get_hash(string x) {
	long long ans = 0;
	for (int i = 0; i < x.size(); i++) {
		ans = (ans * pep % mod1 + x[i] % mod1) % mod1;
	}
	return ans;
}
long long get_hash1(string x) {
	long long ans = 0;
	for (int i = 0; i < x.size(); i++) {
		ans = (ans * pep % mod2 + x[i] % mod2) % mod2;
	}
	return ans;
}
int main() {
	cin >> t;
	p[0] = 1;
	pp[0] = 1;
	for (int i = 1; i <= 100005; i++) p[i] = p[i - 1] * pep % mod1;
	for (int i = 1; i <= 100005; i++) pp[i] = pp[i - 1] * pep % mod2;
	while(t--) {
		int ans = 0;
		memset(h, 0, sizeof(h));
		memset(hh, 0, sizeof(hh));
		cin >> a >> b;
		long long c = get_hash(a);
		long long e = get_hash1(a);
		int lena = a.size();
		h[1] = b[0];
		h[0] = 0;
		hh[0] = 0;
		hh[1] = b[0];
		for (int i = 2; i <= b.size(); i++) {
			h[i] = (h[i - 1] * pep % mod1 + b[i - 1] % mod1) % mod1;
		}
		for (int i = 2; i <= b.size(); i++) {
			hh[i] = (hh[i - 1] * pep % mod2 + b[i - 1] % mod2) % mod2;
		}
		for (int i = lena; i <= b.size(); i++) {
			long long d = 0;
			long long d1 = 0;
			d = (h[i] % mod1 - h[i - lena] * p[lena] % mod1 + mod1) % mod1;
			d1 = (hh[i] % mod2 - hh[i - lena] * pp[lena] % mod2 + mod2) % mod2;
			if (d == c && d1 == e) ans++;
		}
		cout << ans << endl;
	}
	return 0;
}

最后,引用著名奥赛教练员 $ Huge $ 的一句话来证明 $ Hash $ 的重要性:“ 很多题都能用 $ Hash $ 直接过去 ”;

KMP

$ KMP $是用来求一个格式串在一个文本串中出现的所有位置;

首先引入一些概念:

$ Border $ :如果一个串 $ A $ 同时是一个串 $ B $ 的真前缀和真后缀,则称串 $ A $ 是串 $ B $ 的 一个 $ Border $;

前缀函数:对于一个串 $ A $,其前缀函数为串 $ A $ 的最大 $ Border $ 的长度;

回到问题,求一个格式串在一个文本串中出现的所有位置,我们可以维护两个指针,每次扩展一位,如果失配,则跳转到这个长度的 $ Border $ 处(这里证明不想写了,可以参考别的博客);

所以我们可以维护一个数组 $ pi[i] $ 代表下标为 $ i $ 时,$ i $ 及其前面的字符串的前缀函数;

例题

求前缀函数,最后递归输出;

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n;
char s[500005];
int pii[500005];
void pi(char c[]) {
	int len = strlen(c + 1);
	for (int i = 2; i <= len; i++) {
		int j = pii[i - 1];
		while(j && c[i] != c[j + 1]) j = pii[j];
		if (c[i] == c[j + 1]) j++;
		pii[i] = j;
	}
}
void out(int x) {
	if (!x) return;
	out(pii[x]);
	cout << x << ' ';
}
int main() {
	while(cin >> (s + 1)) {
		n = strlen(s + 1);
		pi(s);
		out(n);
		cout << '\n';
		for (int i = 1; i <= n; i++) pii[i] = 0;
	}
	return 0;
}

image

维护一个栈,应用 $ KMP $ 的思想,每次匹配上后就将其弹出,同时跳转到现在的栈顶所能匹配到的最大位置(下标 + 1);

这里维护一个数组记录一下此位置所能匹配到的最大位置即可;

#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
using namespace std;
int st[10000005], top;
char t[1000005], s[1000005];
int pi[10000005];
int f[10000005];
int tlen, slen;
void pii(char x[]) {
	for (int i = 2; i <= slen; i++) {
		int j = pi[i - 1];
		while(j != 0 && x[i] != x[j + 1]) j = pi[j];
		if (x[i] == x[j + 1]) j++;
		pi[i] = j;
	}
}
int main() {
	scanf("%s %s", t + 1, s + 1);
	tlen = strlen(t + 1);
	slen = strlen(s + 1);
	pii(s);
	int j = 0;
	for (int i = 1; i <= tlen; i++) {
		st[++top] = i;
		while(j > 0 && t[i] != s[j + 1]) j = pi[j];
		if (t[i] == s[j + 1]) j++;
		f[i] = j;
		if (j == slen) {
			top -= slen;
			j = f[st[top]];
		}
	}
	for (int i = 1; i <= top; i++) cout << t[st[i]];
	return 0;
}

KMP进阶

KMP树

就是将一个字符串 $ S $ 的前缀 $ S_i $ 和它的一个极长 $ Border \ S_j $ 的 $ i, j $ 连边,这样会得到一个以 $ 0 $ 为跟的树,且 $ j $ 是 $ i $ 的父亲;

然后就可以解决一些问题;

例题:

image

把 $ KMP $ 树建出来,然后求每 $ k $ 个点的 $ LCA $ 的深度的平方和即可,最后乘上方案数(总的减去每棵子树的);

直接枚举 $ LCA $ 即可;

时间复杂度:$ \Theta(n) $;

点击查看代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const long long mod = 998244353;
int k;
int n;
char s[1000005];
long long fac[1000005], fav[1000005];
inline long long ksm(long long a, long long b) {
	long long ans = 1;
	while(b) {
		if (b & 1) ans = ans * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return ans;
}
inline long long C(long long a, long long b) {
	if (a < b) return 0;
	if (b < 0) return 0;
	return fac[a] * fav[b] % mod * fav[a - b] % mod;
}
long long ans;
int pi[1000005];
struct sss{
	int t, ne;
}e[2000005];
int h[2000005], cnt;
inline void add(int u, int v) {
	e[++cnt].t = v;
	e[cnt].ne = h[u];
	h[u] = cnt;
}
inline void KMP() {
	int j = 0;
	add(0, 1);
	for (int i = 2; i <= n; i++) {
		while(j && s[i] != s[j + 1]) j = pi[j];
		if (s[i] == s[j + 1]) j++;
		pi[i] = j;
		add(j, i);
	}
}
int dep[1000005], siz[1000005];
long long sum[1000005];
void dfs(int x, int de) {
	dep[x] = de;
	siz[x] = 1;
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		dfs(u, de + 1);
		siz[x] += siz[u];
		sum[x] = (sum[x] + C(siz[u], k)) % mod;
	}
}
int main() {
	freopen("string.in", "r", stdin);
	freopen("string.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> k;
	cin >> (s + 1);
	n = strlen(s + 1);
	fac[0] = 1;
	fav[0] = 1;
	for (int i = 1; i <= n + 1; i++) {
		fac[i] = fac[i - 1] * i % mod;
		fav[i] = ksm(fac[i], mod - 2);
	}
	KMP();
	dfs(0, 1);
	for (int i = 0; i <= n; i++) {
		if (siz[i] < k) continue;
		long long res = (C(siz[i], k) - sum[i] + mod) % mod;
		ans = (ans + res * dep[i] % mod * dep[i] % mod) % mod;
	}
	cout << ans;
	return 0;
}

求不重合的 $ Border $;

就是这道题:Luogu P2375 [NOI2014] 动物园

可以 $ \Theta(n) $ 用 $ KMP $ 做,具体就是多维护一个数组 $ sum $ 记录为达到这个前缀数组跳了多少次(其实就是它在 $ KMP $ 树上的深度),然后每次跳到第一个 $ j \times 2 \leq i $ 的位置统计这个 $ sum $ 作为答案即可;

Trie树

查找一个单词在给出的所有单词中是否出现,出现的次数等等;

image

转化成二进制比较,遇到不同的就计算 $ ans $;

#include <iostream>
#include <cstring>
using namespace std;
int n;
int a;
int son[10000005][5];
long long ans[10000005];
int sum;
int cnt;
void add(int xx) {
	int now = 1;
	for (int i = 31; i >= 0; i--) {
		if (son[now][(xx >> i) & 1] == 0) son[now][(xx >> i) & 1] = ++cnt;
		now = son[now][(xx >> i) & 1];
	}
}
void w(int x, int o) {
	int now = 1;
	for (int i = 31; i >= 0; i--) {
		int k = (x >> i) & 1;
		if (son[now][!k] == 0) {
			now = son[now][k];
		} else {
			ans[o] += (1 << i);
			now = son[now][!k];
		}
	}
	add(x);
}
int main() {
	cin >> n;
	memset(ans, 0, sizeof(ans));
	sum = 0;
	cnt = 1;
	for (int i = 1; i <= n; i++) {
		cin >> a;
		sum++;
		w(a, sum);
	}
	long long an = 0;
	for (int i = 1; i <= sum; i++) {
		an = max(an, ans[i]);
	}
	cout << an;
}
posted @ 2024-05-08 11:07  Peppa_Even_Pig  阅读(37)  评论(3编辑  收藏  举报