哈希表【hash】
一.哈希表的插入及查询
hash表是一种数据结构,又称为散列表,其根本的原理就是把一个数变成另外一个易于存储的数。
先来看一道例题吧
假如有n个数,n的范围只有10万,但是每个数的大小有1e9,怎么做才能统计每个数出现的次数呢?
如果每个数的范围很小,则可以开一个数组(俗称为“桶”)存储。
但是 现在,根本开不了1e9的数组。而且,总共就1e5个数字,如果开1e9的数组,那么至少会浪费500000000个位置,就太不值得了。
事实上,根本没必要开那么大,只要能存1e5个数字就可以了。
于是我们就要把大小为1e9的数字映射到1e5
这就是hash的应用了。
具体映射的方法有很多,但最重要的就是取模法.
显而易见,只需要把每一个数模上一个1e5左右的数,就可以了。
如果H表示哈希函数,那么这个式子就是
H ( x ) = x % mod
一般情况下,模的这个数一般是一个质数。
这是为什么呢?
因为不同的两个数取模可能会得到同一结果,例如13和25,模数为12,则取模的结果都是1,存储时就会覆盖掉。
这被称为哈希冲突。
因此,摸一个质数可以减少冲突的概率,具体的原因可以搜一下。
不过这是尽可能的减少,并不能避免,所以,接下来介绍两种解决哈希冲突的方法。
1:拉链法
将所有取模后结果相同的都放在一个单链表里,查询时直接在单链表里查就行了。
类似于邻接表(链式前向星)。
不过我个人更倾向于第二种
2:开放地址法
通俗的来说,其实就是模过之后,如果这个位置已经有数字,那么就加上一个数,再取模,一直重复,直到找到空位。
也就是
H(x)=(x%mod+k)%mod
代码实现插入x
void Insert(int x)
{
int p=H(x);
while(hash[p]&&(hash[p]!=x)) p=(113+p)%10007;
hash[p]=x;
}
查询是否存在x
bool Query(int x)
{
int p=H(x);
while(hash[p]&&(hash[p]!=x)) pce=(113+p)%10007;
if(hash[p]!=0) return 1;
return 0;
}
这也很好理解,当找到一个空位时,如果插入时有这个元素那么肯定会插入到空位中,因此之前没有插入过这个元素。
如果跳的时候直接找到了,就返回真即可。
二.哈希表的应用
1.随机的点
题目描述
陶陶为了给一道平面毒瘤计算几何题出数据,需要产生 N 个点(x[i],y[i])。已知x,y是由伪随机函数顺序产生,即:
X[i+1]=(X[i]∗Ax+Bx+i)%Cx(X[1],Ax,Bx,Cx是事先给定的)
Y[i+1]=(Y[i]∗Ay+By+i)%Cy(Y[1],Ay,By,Cy是事先给定的)
这样,就可以快速连续产生很多点坐标(X[i], Y[i])。 不幸的是,这样产生的点有可能有相同的,虽然这种几率很少,但严谨的陶陶不允许这种事发生。陶陶要求你帮助他解决最少要产生前多少项时,正好有 N 个不相同的点。
输入格式
第一行。一个整数 N .
第二行:4个整数X[1],Ax,Bx,Cx
第三行:4个整数Y[1],Ay,By,Cy
输出格式
一个整数 M 。表示最少要连续产生 M 个点,正好有 N 个不相同的点。数据保证有答案。
样例数据
input
21
2 4 3 6
5 2 3 13
output
24(原始样例为25,请大家实验)
数据规模与约定
1<=N<=1,000,000, 其它所有数据都在[1...1,000,000,000]范围内。
时间限制:1texts
空间限制:256textMB
题解
本题的关键在于如何存储两个值。
于是我们可以把这对坐标用类似于K进制存起来,再进行哈希。
long long Trydent(long long x,long long y)
{
return (x*131%200351+y*131*131%2000351)%2000351;
}
完整代码如下
#include<bits/stdc++.h>
using namespace std;
long long s,k;
struct node
{
long long x,y,v;
}pce[5000100];
long long HASH(long long x)
{
return (x+3045173)%2323237;
}
long long Trydent(long long x,long long y)
{
return (x*131%200351+y*131*131%2000351)%2000351;
}
long long n,sum=1,cnt=1;
long long x,y,a1,b1,c1,a2,b2,c2;
int main()
{
freopen("distinct.in","r",stdin);
freopen("distinct.out","w",stdout);
cin>>n;
cin>>x>>a1>>b1>>c1;
cin>>y>>a2>>b2>>c2;
while(sum<n)
{
x=(1ll*x*a1+b1+cnt)%c1;
y=(1ll*y*a2+b2+cnt)%c2;
s=HASH(Trydent(x,y));
if(pce[s].v==0)
{
pce[s].v=1;
pce[s].x=x;
pce[s].y=y;
sum++;
}
else
{
k=HASH( Trydent( pce[s].x , pce[s].y ) );
while(pce[k].v&&(pce[k].x!=x||pce[k].y!=y))
{
k=HASH(k);
}
if(pce[k].v==0)
{
sum++;
pce[k].v=1;
pce[k].x=x;
pce[k].y=y;
}
}
cnt++;
}
cout<<cnt;
return 0;
}
不过在更多的题中,哈希只是一种辅助,并不是主要的。
2.洛谷P6273 [eJOI2017]魔法
题目描述
给定一个长度为 n 的字符串 S。设 S 中不同的字符数为 k 。
定义字符串的子串为该字符串某一连续段。
而 有魔法的子串 被定义为 S 的某一非空子串,满足该子串中不同的字符数为 k ,且每个字符的出现的次数都相同。
你需要求出给定字符串 S 的不同的 有魔法的子串 的个数。
若两个子串的左右端点不同,则这两个子串不同。
输入格式
第一行:一个整数 n 表示字符串长度。
第二行:一个字符串 S 。
输出格式
一个整数表示答案 mod(1e9+7) 的值。
输入输出样例
输入 #1复制
8 abccbabc
输出 #1复制
4
输入 #2复制
7 abcABCC
输出 #2复制
1
输入 #3复制
20 SwSSSwwwwSwSwwSwwwwS
输出 #3复制
22
说明/提示
【输入输出样例解释】
样例 1 解释
- 满足条件的子串有: abc,cba,abc,abccba
样例 2 解释
- 仅子串abcABC 为 有魔法的子串(区分大小写,即 a不等于A)。
样例 3 解释
- 其中一个是 SwSwwS。
【数据规模与约定】
本题采用多测试点捆绑测试,共有 4 个子任务。
- Subtask 1(10 points):2≤n≤100。
- Subtask 2(20 points):2≤n≤2×1e3。
- Subtask 3(30 points):2≤n≤1e5,k=2 (即 S 中只有两种字符)。
- Subtask 4(40 points):无其他限制。
对于所有数据,保证 2≤n≤1e5,字符集为[a,z]∪[A,Z]
【说明】
原题来自:eJOI 2017 Problem A Magic
翻译提供:@_Wallace_
题解
如果设sum[i][ch]表示1~i 的字符中有多少个字符ch。
则很明显 题中的子串满足 所有的sum[i][ch]-sum[j-1][ch]都相等
例如 sum[i][a]-sum[j-1][a]=sum[i][b]-sum[j-1][b]
则 sum[i][a]-sum[i][b]=sum[j-1][a]-sum[j-1][b]
换句话说这段区间的两个端点任意两种字符个数对应的差都是相等的。
因此我们可以处理出sum数组,然后用相邻的作差,一共k-1个就可以了,然后用类似第一题的方法哈希出来。当两个位置的哈希值完全相同时,则这个子串是有魔法的。
for(int i=1;i<=n;i++)
{
for(int j=1;j<=k;j++)
sum[i][ch[j]]=sum[i-1][ch[j]];//ch表示第i种字符
sum[i][s[i]]++;
for(int j=1;j<k;j++)
New[i][j]=sum[i][ch[j]]-sum[i][ch[i+1]];
}
本题不提供完整代码,嘿嘿。
三.字符串哈希及其应用
字符串哈希,就是把字符串转换成一个数字存起来。其思想就是把字符串看成一个K进制的数,也就是a=1,b=2,c=3……z=26,然后再取模
只不过为了避免低效的取模运算,通常用unsigned long long 存储,溢出时会自动取模
一般,我们的K取131或13331,此时哈希冲突产生的概率极低(几乎没有)。
这样我们就可以得到一个字符串的哈希值
那么接着思考,如果已经知道了字符串S的hash值H(S),T的hash值H(T),那么字符串S+T的哈希值能否求出呢?
H(S+T)=(H(S)*K+H(T))%vmod
同理
H(T)=(H(S+T)-H(S)*K)% mod
通过这两种操作,我们可以预处理一个字符串的所有前缀字符串的hash值并通过第二种操作得到每一段字符的hash值
兔子与兔子
题目描述
很久很久以前,森林里住着一群兔子。有一天,兔子们想要研究自己的 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
数据规模与约定
时间限制:1s
空间限制:256MB
很简单,只要用刚才的代码就能求出每一段的hash值,当两段hash相同时就视为相同
#include<bits/stdc++.h>
using namespace std;
unsigned long long f[1000020],p[1000200],a,b;
char c[1000200];
int l1,l2,r1,r2,s,n;
int main()
{
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
scanf("%s",c+1);
s=strlen(c+1);
p[0]=1;
for(int i=1;i<=s;i++)
{
f[i]=f[i-1]*131+(c[i]-'a'+1);
p[i]=p[i-1]*131;
}
scanf("%d",&n);
while(n--)
{
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
if(f[r1]-f[l1-1]*p[r1-l1+1]==f[r2]-f[l2-1]*p[r2-l2+1]) printf("Yes\n");
else printf("No\n");
}
return 0;
}