Hash 表
Hash 表就是将集合的域很大的映射到小的域上。所以使用它就很产生两个问题:一、如何映射;二、如何处理不同元素的相同映射。
-
对于问题一,我们可以使用一个 H a s h Hash Hash 函数进行进行映射,对于整数域而言,通常这个 H a s h Hash Hash 函数就是取模。
-
对于问题二,我们可以利用邻接表的数据结构来表示 H a s h Hash Hash 表,为每一个映射开一个链表,这样相同映射的元素就会在同一个链表中,解决冲突。
典型的用法就是记录一个序列中每个整数出现的次数,整数范围不大,那么直接用数字当下标就行了,但是如果数字太大了,就可以使用 H a s h Hash Hash 表将数字映射到它的 H a s h Hash Hash 值,在这里储存它的次数。
【例题】雪花雪花雪花
有 N N N 片雪花,每片雪花由六个角组成,每个角都有长度。
第 i i i 片雪花六个角的长度从某个角开始顺时针依次记为 a i , 1 , a i , 2 , … , a i , 6 a_{i,1},a_{i,2},…,a_{i,6} ai,1,ai,2,…,ai,6。
因为雪花的形状是封闭的环形,所以从任何一个角开始顺时针或逆时针往后记录长度,得到的六元组都代表形状相同的雪花。
例如 a i , 1 , a i , 2 , … , a i , 6 a_{i,1},a_{i,2},…,a_{i,6} ai,1,ai,2,…,ai,6 和 a i , 2 , a i , 3 , … , a i , 6 , a i , 1 a_{i,2},a_{i,3},…,a_{i,6},a_{i,1} ai,2,ai,3,…,ai,6,ai,1 就是形状相同的雪花。
a i , 1 , a i , 2 , … , a i , 6 a_{i,1},a_{i,2},…,a_{i,6} ai,1,ai,2,…,ai,6 和 a i , 6 , a i , 5 , … , a i , 1 a_{i,6},a_{i,5},…,a_{i,1} ai,6,ai,5,…,ai,1 也是形状相同的雪花。
我们称两片雪花形状相同,当且仅当它们各自从某一角开始顺时针或逆时针记录长度,能得到两个相同的六元组。
求这 N N N 片雪花中是否存在两片形状相同的雪花。
数据范围
1
≤
N
≤
100000
,
0
≤
a
i
,
j
<
10000000
1≤N≤100000, 0≤a_{i,j}<10000000
1≤N≤100000,0≤ai,j<10000000
分析:
为了能提高从集合中找出同类型的雪花的效率,因此使用 H a s h Hash Hash 表存储相同映射的雪花。所以设计 H a s h Hash Hash 函数为 H ( a i , 1 , a i , 2 , … , a i , 6 ) = ( ∑ j = 1 6 a i , j + ∏ j = 1 6 a i , j ) m o d P H(a_{i,1},a_{i,2},…,a_{i,6}) = (\sum_{j=1}^6a_{i,j}+\prod_{j=1}^6a_{i,j})~mod~P H(ai,1,ai,2,…,ai,6)=(∑j=16ai,j+∏j=16ai,j) mod P , P P P 为一个较大的质数。
于是我们对于每一个插入 H a s h Hash Hash 表的雪花,都会查看是否该 H a s h Hash Hash 值已存在。并在这个 H a s h Hash Hash 值下的链表中找出是否有相同类型的雪花,因为雪花具有旋转的特性,所以不提前处理储存的雪花进行比较的话,其比较的事件复杂度为 O ( n 2 ) O(n^2) O(n2) ,如果利用字符串的最小表示法则可化为 O ( n ) O(n) O(n) 时间复杂度。
代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 300005;
int n;
char s[N];
ULL h[N], p[N];
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int sa[N];
int get_m_s(int k1, int k2) {
int l = 0, r = min(n - k1 + 1, n - k2 + 1);
while(l < r) {
int mid = (l + r + 1) >> 1;
if(get(k1, k1 + mid - 1) == get(k2, k2 + mid - 1))
l = mid;
else r = mid - 1;
}
return l;
}
bool cmp(const int &x1, const int &x2) {
int d = get_m_s(x1, x2);
int x1_v = d > (n - x1 + 1) ? -1 : s[x1 + d];
int x2_v = d > (n - x2 + 1) ? -1 : s[x2 + d];
return x1_v < x2_v;
}
int main()
{
scanf("%s", s + 1);
n = strlen(s + 1);
p[0] = 1;
for(int i = 1; i <= n; ++i) {
h[i] = h[i - 1] * 131 + s[i] - 'a' + 1;
p[i] = p[i - 1] * 131;
}
for(int i = 1; i <= n; ++i)
sa[i] = i;
sort(sa + 1, sa + 1 + n, cmp);
for(int i = 1; i <= n; ++i) {
cout << sa[i] - 1 << ' ';
}
cout << endl;
for(int i = 1; i <= n; ++i) {
cout << get_m_s(sa[i], sa[i - 1]) << ' ';
}
return 0;
}
字符串哈希
在字符串匹配问题中,如果只是暴力匹配,其时间复杂度就是 O ( N M ) O(NM) O(NM) ,使用 K M P KMP KMP 可以变为 N + M N+M N+M ,但是字符串哈希可以变为 O ( N ) O(N) O(N) 。
将一个字符串看成是一个 P P P 进制数,对于每个字符对应一个大于 0 0 0 的数值。一般来说,这个数值肯定是 P P P 进制内的数。
比如对于小写字母中 a = 1 a=1 a=1 、 b = 2 b = 2 b=2 、 c = 3 c = 3 c=3 … z = 26 z=26 z=26 。为了避免数值太大,所以需要把这些数映射到一个有限的区间中,比如 [ 0 , 2 64 ] [0,2^{64}] [0,264] 。
选取进制时也是有讲究的,通常取 P = 131 P=131 P=131 或 p = 13331 p=13331 p=13331 ,此时冲突概论极低。
对于我们已知的字符串 S S S 的 H a s h Hash Hash 值为 H ( S ) H(S) H(S) ,那么在 S S S 后添加一个字符 c c c 构成的新字符串 S + c S + c S+c 的 H a s h Hash Hash值就是 H ( S + c ) = ( H ( S ) × P + v a l u e ( c ) ) m o d M H(S +c) = (H(S)\times P+value(c))~mod~M H(S+c)=(H(S)×P+value(c)) mod M 。
如果已知了 H a s h Hash Hash 值 H ( S ) H(S) H(S) 和 H ( S + T ) H(S+T) H(S+T) 后,那么对于 H a s h Hash Hash 值 H ( T ) = ( H ( S + T ) − H ( S ) × P l e n g t h ( T ) ) m o d M H(T) = (H(S+T) - H(S)\times P^{length(T)})~mod~M H(T)=(H(S+T)−H(S)×Plength(T)) mod M 。
因此我们可以预处理出字符串 S S S 的所有前缀的 H a s h Hash Hash 值,之后就可以 O ( 1 ) O(1) O(1) 的时间复杂度来获取任意子串的 H a s h Hash Hash 值了。
【例题】兔子与兔子
很久很久以前,森林里住着一群兔子。
有一天,兔子们想要研究自己的 D N A DNA DNA 序列。
我们首先选取一个好长好长的 D N A DNA DNA 序列(小兔子是外星生物, D N A DNA DNA 序列可能包含 26 26 26 个小写英文字母)。
然后我们每次选择两个区间,询问如果用两个区间里的 D N A DNA DNA 序列分别生产出来两只兔子,这两个兔子是否一模一样。
注意两个兔子一模一样只可能是他们的 D N A DNA DNA 序列一模一样。
数据范围
1
≤
l
e
n
g
t
h
(
S
)
,
m
≤
1000000
1≤length(S),m≤1000000
1≤length(S),m≤1000000
分析:
对于两者的 D N A DNA DNA 序列,预处理出它们的 H a s h Hash Hash 值 ,然后对于每次询问就是 O ( 1 ) O(1) O(1) 时间复杂度。
代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 1000005;
const int P = 131;
ULL h[N], p[N];
void init(string &str) {
int n = str.size();
p[0] = 1;
for(int i = 1; i <= n; ++i) {
h[i] = h[i - 1] * P + str[i - 1] - '0' + 1;
p[i] = p[i - 1] * P;
}
}
ULL get(int l, int r) // 计算子串 str[l ~ r] 的哈希值
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
string str;
cin >> str;
init(str);
int t;
cin >> t;
while(t--) {
int l1, r1, l2, r2;
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
if(get(l1, r1) == get(l2, r2)) {
puts("Yes");
} else puts("No");
}
return 0;
}
【例题】回文子串的最大长度
如果一个字符串正着读和倒着读是一样的,则称它是回文的。
给定一个长度为 N N N 的字符串 S S S,求他的最长回文子串的长度是多少。
分析:
如果这个字符串具有长度为
n
n
n 的回文子串,那么对于小于等于
n
n
n 的子串都存在一个回文串 。
但是对于奇回文串、偶回文串的长度变化不一致,所以要两者分别处理。
故长度具有单调性,所以问题转换为二分答案问题了,现在就是如何快速的解决这个判定问题了,使用字符串哈希显然可以使得原本 n 2 n^2 n2 的时间复杂度将为 O ( n ) O(n) O(n) ,乘上 ( l o g ( n ) ) (log(n)) (log(n)) 的判定,于是问题就可以解决了。
代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 1000005;
const int P = 131;
ULL lh[N], p[N], rh[N];
char s[N];
void init(int n) {
for(int i = 1; i <= n; ++i) {
lh[i] = lh[i - 1] * P + s[i] - 'a' + 1;
p[i] = p[i - 1] * P;
}
rh[n + 1] = 0;
for(int i = n; i >= 1; --i) {
rh[i] = rh[i + 1] * P + s[i] - 'a' + 1;
}
}
ULL getL(int l, int r) {
return lh[r] - lh[l - 1] * p[r - l + 1];
}
ULL getR(int l, int r) // 计算子串 str[l ~ r] 的哈希值
{
return rh[l] - rh[r + 1] * p[r - l + 1];
}
bool checkO(int x, int n) {
for(int i = x + 1; i + x <= n; ++i) {
if(getL(i - x, i) == getR(i, i + x)) {
return true;
}
}
return false;
}
bool checkE(int x, int n) {
for(int i = x + 1; i + x - 1 <= n; ++i) {
if(getL(i - x, i - 1) == getR(i, i + x - 1)) {
return true;
}
}
return false;
}
int main()
{
p[0] = 1;
int tt = 1;
while(scanf("%s", s + 1) &&
!(s[1] == 'E' && s[2] == 'N' && s[3] == 'D')) {
int n = strlen(s + 1);
init(n);
int p, q;
int l = 0, r = n / 2 + 1;
while(l < r) {
int mid = (l + r + 1) >> 1;
if(checkO(mid, n)) l = mid;
else r = mid - 1;
}
p = l;
l = 0, r = n / 2 + 1;
while(l < r) {
int mid = (l + r + 1) >> 1;
if(checkE(mid, n)) l = mid;
else r = mid - 1;
}
q = l;
printf("Case %d: %d\n", tt++, max(2 * p + 1, 2 * q));
}
return 0;
}
【例题】后缀数组
后缀数组 ( S A ) (SA) (SA) 是一种重要的数据结构,通常使用倍增或者 D C 3 DC3 DC3 算法实现,这超出了我们的讨论范围。
在本题中,我们希望使用快排、 H a s h Hash Hash 与二分实现一个简单的 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n) 的后缀数组求法。
详细地说,给定一个长度为 n n n 的字符串 S S S(下标 0 0 0∼ n − 1 n−1 n−1),我们可以用整数 k ( 0 ≤ k < n ) k(0≤k<n) k(0≤k<n) 表示字符串 S S S 的后缀 S ( k ∼ n − 1 ) S(k∼n−1) S(k∼n−1)。
把字符串 S S S 的所有后缀按照字典序排列,排名为 i i i 的后缀记为 S A [ i ] SA[i] SA[i]。
额外地,我们考虑排名为 i i i 的后缀与排名为 i − 1 i−1 i−1 的后缀,把二者的最长公共前缀的长度记为 H e i g h t [ i ] Height[i] Height[i]。
我们的任务就是求出 S A SA SA 与 H e i g h t Height Height 这两个数组。
分析:
题目其实已经给出了算法了,只不过要做到
O
(
n
l
o
g
2
n
)
O(nlog^2n)
O(nlog2n) ,就得在快速排序的比较环节优化了。
因为这个后缀数组是在同一个字符串上,所以预处理出该字符串的
H
a
s
h
Hash
Hash 值后,对于该字符串上的任意两个子串,我们都可以理由
O
(
l
o
g
n
)
O(logn)
O(logn) 时间复杂度来找出它们的最长公共前缀,这样比较就可以
O
(
l
o
g
n
)
O(logn)
O(logn) 比较出两字符串的字典序了。
代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 300005;
int n;
char s[N];
ULL h[N], p[N];
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int sa[N];
int get_m_s(int k1, int k2) {
int l = 0, r = min(n - k1 + 1, n - k2 + 1);
while(l < r) {
int mid = (l + r + 1) >> 1;
if(get(k1, k1 + mid - 1) == get(k2, k2 + mid - 1))
l = mid;
else r = mid - 1;
}
return l;
}
bool cmp(const int &x1, const int &x2) {
int d = get_m_s(x1, x2);
int x1_v = d > (n - x1 + 1) ? -1 : s[x1 + d];
int x2_v = d > (n - x2 + 1) ? -1 : s[x2 + d];
return x1_v < x2_v;
}
int main()
{
scanf("%s", s + 1);
n = strlen(s + 1);
p[0] = 1;
for(int i = 1; i <= n; ++i) {
h[i] = h[i - 1] * 131 + s[i] - 'a' + 1;
p[i] = p[i - 1] * 131;
}
for(int i = 1; i <= n; ++i)
sa[i] = i;
sort(sa + 1, sa + 1 + n, cmp);
for(int i = 1; i <= n; ++i) {
cout << sa[i] - 1 << ' ';
}
cout << endl;
for(int i = 1; i <= n; ++i) {
cout << get_m_s(sa[i], sa[i - 1]) << ' ';
}
return 0;
}