字符串Hash入门
引言
当我们对于一组值域较大但数据总数较少的数据无法处理时,其中一个处理方法就是对其进行离散化,把大值映射到小值内。但是,如果这组数据是由字符串组成的,我们又该如何对其处理?字符串Hash就是一种将字符串进行“离散化”,将字符串映射到一个可以接受的数值内,方便对数据进行处理。
做法
假设我们现在要将一个字符串\(S\)通过字符串\(Hash\)映射到一个值内。
我们可以将\(S\)看作一个\(P\)进制数,对\(S\)中的每个字符赋予一个权值。
假设\(S\)是由小写字符组成的,那么对于每一个字符,我们可以将\(a\)到\(z\)赋值为\(1\)到\(26\)。
举个例子,当\(P\)取\(26\),\(S\)取\(aab\)时,它的Hash值就是\(1*26^2+1*26^1+2*26^0\),并对这个值适当取模。
对于一个字符串\(dsakhdkas\)来说,它的Hash值为\((4\ 19\ 1\ 11\ 8\ 4\ 11\ 1\ 19)_{26}\),则
\(Hash_0=0\)
\(Hash_1=4\)
\(Hash_2=4*26+19\)
\(Hash_3=4*26^2+19*26+1\)
\(Hash_4=4*26^3+19*26^2+1*26^1+11\)
\(Hash_n=...\)
如果要取\(S_{2-4}\)(sak),则它的\(Hash\)值为\((4\ 19\ 1\ 11)_{26}-(4\ 0\ 0\ 0)_{26}=(0\ 19\ 1\ 11)_{26}\),即\(Hash_4-Hash_1*26^3\)
故对于一个字符串\(S\),\(S_{l-r}\)的\(Hash\)值为\(Hash_r-Hash_{l-1}*P^{r-l+1}\)
为了减少\(Hash\)的冲突,建议取一个质数作为\(P\),通常情况下取\(131\)或\(13331\)。而且在方便且准确的情况下,我们可以直接将\(Hash\)数组和\(P\)的若干次方开为\(unsigned long long\)或\(unsigned int\),使之自然溢出。
例题
兔子与兔子
模板题,直接上代码:
// 代码中f即为Hash数组,P为131的若干次方
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ll;
#define mp make_pair
#define il inline
#define ri register int
#define rep(i, a, b) for (ri i = a; i <= b; ++ i)
#define per(i, a, b) for (ri i = a; i >= b; -- i)
const int N = 1000009;
int len, n;
ll p[N], f[N];
char s[N];
il char gc() {
static char ibuf[1048576], *iS, *iT;
return (iS == iT ? (iT = (iS = ibuf) + fread(ibuf, 1, 1048576, stdin), (iS == iT ? EOF : *iS ++)) : *iS ++);
}
void readstr() {
register char c = gc();
while (c >= 'a' && c <= 'z') s[++ len] = c, c = gc();
}
void pre() {
p[0] = 1;
rep (i, 1, len) {
p[i] = p[i - 1] * 131;
f[i] = f[i - 1] * 131 + s[i] - 'a' + 1;
}
}
template<class I>
il void read(I &x) {
register char c = gc(); x = 0; ri f = 0;
while(!isdigit(c)) f = (c == '-') ? 1 : f, c = gc();
while(isdigit(c)) x = (x << 3) + (x << 1) + (c & 15), c = gc();
x = f ? -x : x;
}
int main() {
readstr();
pre();
read(n);
while (n --) {
int l1, r1, l2, r2;
read(l1), read(r1), read(l2), read(r2);
if (f[r1] - f[l1 - 1] * p[r1 - l1 + 1] == f[r2] - f[l2 - 1] * p[r2 - l2 + 1]) puts("Yes");
else puts("No");
}
return 0;
}
再上一道模板题
珍珠项链
给出一个字符串\(S\),将其平均分成若干段,询问每段长度为多少时有最多不同的字符串数(每一段可以反转)
这题只要正反各做一遍\(Hash\),暴力枚举长度即可。时间复杂度\(O(nln_n)\)
#include <bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define rep(i, a, b) for (ri i = a; i <= b; ++i)
#define per(i, a, b) for (ri i = a; i >= b; --i)
const int N = 200009;
typedef unsigned long long ll;
int n, a[N], res, vis[N], cnt;
ll p[N], f1[N], f2[N];
vector<int> vec, output;
vector<ll> h[1000009];
il char gc() {
static char ibuf[1048576], *iS, *iT;
return (iS == iT ? (iT = (iS = ibuf) + fread(ibuf, 1, 1048576, stdin), (iS == iT ? EOF : *iS++)) : *iS++);
}
template <class I>
il void read(I &x) {
register char c = gc();
x = 0;
ri f = 0;
while (!isdigit(c)) f = (c == '-') ? 1 : f, c = gc();
while (isdigit(c)) x = (x << 3) + (x << 1) + (c & 15), c = gc();
x = f ? -x : x;
}
il ll hasha(int l, int r) { return f1[r] - f1[l - 1] * p[r - l + 1]; }
il ll hashb(int l, int r) { return f2[l] - f2[r + 1] * p[r - l + 1]; }
il int Hash(ll x) { return x % 999997 + 1; }
int check(int x1, ll y1, int x2, ll y2) {
rep(i, 0, (int)h[x1].size() - 1) if (h[x1][i] == y1) return 0;
rep(i, 0, (int)h[x2].size() - 1) if (h[x2][i] == y2) return 0;
return 1;
}
int main() {
read(n);
p[0] = 1;
rep(i, 1, n) {
read(a[i]);
p[i] = 19260817 * p[i - 1];
f1[i] = f1[i - 1] * 19260817 + a[i];
}
per(i, n, 1) f2[i] = f2[i + 1] * 19260817 + a[i];
rep(len, 1, n) {
int ans = 0;
cnt = 0;
for (ri l = 1; l + len - 1 <= n; l += len) {
int r = l + len - 1;
ll u = hasha(l, r), v = hashb(l, r);
int x = Hash(u), y = Hash(v);
if (check(x, u, y, v)) {
++ans;
h[x].push_back(u);
vis[++cnt] = x;
if (u != v) {
h[y].push_back(v);
vis[++cnt] = y;
}
}
}
if (ans > res) {
output.clear();
res = ans;
output.push_back(len);
} else if (ans == res)
output.push_back(len);
rep(i, 1, cnt) while (h[vis[i]].size()) h[vis[i]].pop_back();
}
printf("%d %d\n", res, output.size());
rep(i, 0, (int)output.size() - 1) printf("%d ", output[i]);
puts("");
return 0;
}