[字符串总结] 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;
}
维护一个栈,应用 $ 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 $ 的父亲;
然后就可以解决一些问题;
例题:
把 $ 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树
查找一个单词在给出的所有单词中是否出现,出现的次数等等;
转化成二进制比较,遇到不同的就计算 $ 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;
}