初见安~这里大概是本狸第一篇关于字符串的专题……QwQ
Hash表
首先Hash是个啥?【我也不认识】就是说定两个质数p和q,把整数或者是字符通过a *p%q之类的操作将其映射到另一个整数,用于查找。其映射值便是0~q - 1。 14的下标:head[0]
而Hash表就是一个类似于邻接表的结构,各个数据表头head就是该数据的映射值,即使是有映射值重复也没关系,像邻接表一样接在同一个表头后面即可。后期若要查找某个数是否存在,查询该数映射值的表头对应的一串数字,看里面有没有该数即可。
可能这么说有点空,我们画个图吧——假设有,有整数1,14,27,19898,232323,1982.则在hash表中就形如:
而在每一个的内部,则是形如:【邻接表式结构】
head数组就不画了……因为就相当于是邻接表的作用了。
这样的话我们要查询某个数是否存在之类的,就可以在其映射值将数据自动分类的情况下在近乎O(1)的时间复杂度内完成。当然,如果Hash函数设计得好,还可以让每个元素的hash值都不一样,效率可以更高。
放一个例题:给你一个数组,求去重过后的数组。有T组数据。
样例:
input:
2
11
1 2 18 3 3 19 2 3 6 5 4
6
1 2 3 4 5 6
output:
1 2 18 3 19 6 5 4
1 2 3 4 5 6
很明显,我们只需要一边读入,一边判断是否在hash表内存在,如果存在则continue;否则放入hash表中,输出这个数。
放一下hash表的存入和查询的模板代码:【hash有函数名重复,所以最好写Hash,避免不必要的CE】
const int p = 13331, q = 10101;//这里的hash函数为:(x + q) % p
struct node {int a, nxt;} Hash[maxn];//hash表的邻接表式结构,一个存值,一个存邻接表nxt
int head[p + 10], cnt = 0;
void insert(int x) {//hash的加入操作
register int key = (x + q) % p;//key为hash函数值
Hash[++cnt].a = x, Hash[cnt].nxt = head[key]; head[key] = cnt;//邻接表式存储
}
bool search(int x) {//hash表的查询操作
register int key = (x + q) % p;//先得到函数值
for(int i = head[key]; i; i = Hash[i].nxt) {//遍历这个函数后面对应的所有数
if(Hash[i].a == x) return true;/如果找到了这个数
}
return false;
}
关于p和q,我说明一下:首先两个数一定是质数,其次,若,则p越大越好,可以让hash值重复的概率尽量小,以提高查询效率。
Hash表的实现通常可以直接用STL中的map代劳,并且代码实现也要简单不少,但是如果真的遇到了刁钻的出题人卡map的常数【map常数不小】,那你也就只好手写Hash表了。
字符串Hash
就如我们文头介绍的——字符串Hash就是把字符串映射成一个整数,用于查询,比较之类的操作。
字符串的hash函数会比较特殊,因为重复的概率远大于普通整数,所以——我们会利用字符串长度划分梯度。
举个例子——有字符串s = aabcdij【均为小写字母】,设每个单独字符对应的hash值为。则有:【从1开始存的字符串】
也就是说,对于一个字符串中的每一个字符,若是第i个【从1开始数】,则对于整个字符串的Hash值的贡献为:
所以我们可以在的复杂度内求出整个字符串的Hash值。
那么怎么求任意子串的Hash值呢?
因为我们的Hash值很明显是一个前缀和的形式,所以很容易想到用。但是因为有梯度差,所以我们真正的值是:。
如果不太好理解,我们可以换一种写法:
假设还是那个字符串:s = aabcdij,我们取l = 3, r = 5,也就是字符串bcd。
把上文的递推式展开的话就是这个样子的:
易得:字符串bcd的Hash值为,也就是。
就这样。
最后良心告诫:strlen()这个函数,要调用的话记得存入整数里调用!!!每次求len的复杂度怎么都有On的,很容易就TLE了!!!牢记牢记。
我们来看个例题吧——
兔子与兔子
Description
很久很久以前,森林里住着一群兔子。
有一天,兔子们想要研究自己的DNA序列。
我们首先选取一个好长好长的DNA序列(小兔子是外星生物,DNA序列可能包含26个小写英文字母)。
然后我们每次选择两个区间,询问如果用两个区间里的DNA序列分别生产出来两只兔子,这两个兔子是否一模一样。
注意两个兔子一模一样只可能是他们的DNA序列一模一样。
Input
第一行输入一个DNA字符串S。
第二行一个数字m,表示m次询问。
接下来m行,每行四个数字l1,r1,l2,r2,分别表示此次询问的两个区间,注意字符串的位置从1开始编号。
数据范围
1≤length(S),m≤1000000
Output
对于每次询问,输出一行表示结果。
如果两只兔子完全相同输出Yes,否则输出No(注意大小写)。
Sample Input
aabbaabb
3
1 3 5 7
1 3 6 8
1 2 1 2
Sample Output
Yes
No
Yes
Sol
这就是一个字符串Hash的典型问题——给你一个字符串,给出两个区间,问你这两个区间的字符串是否相等。这种时候我们只需要得到这两个区间子串的Hash值,看看是否相等即可。复杂度是O(n)的。
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define maxn 1000005
using namespace std;
const int p = 131, q = 2147483643;
typedef unsigned long long ll;
int read() {
int x = 0, f = 1, ch = getchar();
while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
return x * f;
}
string s;
ll sum[maxn];
int n;
ll pwr(ll a, ll b) {//求p的幂次,其实可以开一个数组来存,就不用每次都求一次快速幂了。
ll ans = 1;
while(b) {
if(b & 1) ans = ans * a % q;
b >>= 1;
a = a * a % q;
}
return ans;
}
signed main() {
cin >> s;
for(int i = 0; i < s.size(); i++)//这里的sum数组就是Hash数组。
sum[i + 1] = (1ll * sum[i] * p + s[i] - 'a' + 1) % q;//递推式
register int l1, l2, r1, r2;
ll s1, s2;
n = read();
for(int i = 1; i <= n; i++) {
l1 = read(), r1 = read(), l2 = read(), r2 = read();
s1 = 1ll * (sum[r1] - sum[l1 - 1] * pwr(p, r1 - l1 + 1) % q + q) % q;
s2 = 1ll * (sum[r2] - sum[l2 - 1] * pwr(p, r2 - l2 + 1) % q + q) % q;
//注意这里对于mod q的操作,因为有mod操作后可能会出现负数,所以要+q再mod q。
if(s1 == s2) puts("Yes");
else puts("No");
}
迎评:)
——End——