浅谈哈希
本期题单
前言:
最近几天 Clare613 我又复习了一下哈希,这篇文章算是对这几天的总结。本期题目难度分为为入门和进阶,有搬来的题目和原创题,这里会全部讲完。
哈希简介:
何为哈希:
Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射 pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
有何作用:
依靠本人的理解,就是优化了字符串时间复杂度高的问题,因为在同样的代码中,字符串在使用过程中,可能会多一倍
实现方法:
关于哈希的实现方法,这里我用 ClassIn 画了下图,将就着看吧。\
哈希分为三类,如下图:\
这是我们三种哈希,很明显,只有单哈希是切实可行的。当然,后面会提到“双哈希”,但不是现在写的双哈希。\
有人肯定会问:为什么只写单哈希?原因很简单,因为满哈希冲突率高,而双哈希又要用 map,浪费了时间,没有用处,所以我们以单哈希为主。\
有人肯定又要问了,单哈希怎么实现?我们主要是将字符串转为
#include<bits/stdc++.h>
using namespace std;
long long d=1,sum=0;
int main(){
string a;
cin>>a;
for(int i=a.size()-1;i>=0;i--)
{
if(a[i]=='1'){
sum+=d;
}
d*=2;
}
cout<<sum;
return 0;
}
以上为二进制转十进制,我们现在是字符串转
for(int i=1;i<=n;i++){
a[i]=a[i-1]*13331+x[i];
pw[i]=pw[i-1]*13331;
}
而转换的结果便是:\
\
接下来便是至关重要的问题:我们如果只选取子串该怎么查?我们可以发现,我们假设字符串为:abcde,那么
例题精讲:
这是最长的一部分,如果有人自己本来就会做,就可以跳过了。这里的两部分都有各有五道题,一共十道题,我挑选了其中几道较为重要的题目进行讲解。
P3370 【模板】字符串哈希:
这是最简单一道题目,可以说 map 轻松秒杀,但我们需要用哈希来写,于是得到了这个代码:
#include<bits/stdc++.h>
using namespace std;
int q1=131,q2=13331;
const int mod=999823;
int a[1000005];
int b[1000005];
int hash1(string x){
int sum=0;
for(int i=0;i<x.size();i++){
sum=sum*q1+x[i];
sum%=mod;
}
return sum%mod;
}
int main(){
int n,ans=0;
cin>>n;
for(int i=1;i<=n;i++){
string x;
cin>>x;
if(a[hash1(x)]==1) continue;
ans++;
a[hash1(x)]=1;
}
cout<<ans;
return 0;
}
不难发现,只有
#include<bits/stdc++.h>
using namespace std;
int q1=131,q2=13;
const int mod=999823;
int a[1000005];
int b[1000005];
int hash1(string x){
int sum=0;
for(int i=0;i<x.size();i++){
sum=sum*q1+x[i];
sum%=mod;
}
return sum%mod;
}
int hash2(string x){
int sum=0;
for(int i=0;i<x.size();i++){
sum=sum*q2+x[i];
sum%=mod;
}
return sum%mod;
}
int main(){
int n,ans=0;
cin>>n;
for(int i=1;i<=n;i++){
string x;
cin>>x;
if(a[hash1(x)]==1&&b[hash2(x)]==1) continue;
ans++;
a[hash1(x)]=1;
b[hash2(x)]=1;
}
cout<<ans;
return 0;
}
P10468 兔子与兔子:
这一道题是我们遇到的第一个求子串的题目,按照前面的方法,其实就没有什么难度了,代码如下:
#include<map>
#include<iostream>
#define int unsigned long long
using namespace std;
int a[1000005],pw[1000005];
int hashh(int l,int r){
return a[r]-a[l]*pw[r-l];
}
signed main(){
string x;
cin>>x;
int n,m=x.size();
cin>>n;
x=" "+x;
pw[0]=1;
for(int i=1;i<=m;i++){
a[i]=a[i-1]*13331+x[i];
pw[i]=pw[i-1]*13331;
}
for(int i=1;i<=n;i++){
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(hashh(l1,r1)==hashh(l2,r2)) cout<<"Yes\n";
else cout<<"No\n";
}
return 0;
}
P1481 魔族密码:
这一道题有很多做法,比如说这些大佬。我们的做法是哈希,由于是前缀,所以我们只需要将整体先记录,然后根据经过的数量记录一下,取最大值即可:
#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
map<int,int> mp;
string x[2005];
int a[105],b[2005],pw[105];
int hash1(int l,int r){
return a[r]-a[l-1]*pw[r-l+1];
}
bool cmp(string x,string y){
if(x.size()!=y.size()) return x.size()<y.size();
return x<y;
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cout.tie(0)->sync_with_stdio(0);
int n;
cin>>n;
pw[0]=1;
int ans=0;
for(int i=1;i<=n;i++){
cin>>x[i];
}
sort(x+1,x+n+1,cmp);
for(int i=1;i<=n;i++){
int m=x[i].size();
x[i]=" "+x[i];
int sum=0;
for(int j=1;j<=m;j++){
a[j]=a[j-1]*13331+x[i][j];
pw[j]=pw[j-1]*13331;
sum+=mp[a[j]];
}
mp[a[m]]++;
ans=max(ans,sum);
}
cout<<ans+1;
return 0;
}
P10915 [蓝桥杯 2024 国 B] 最长回文前后缀:
这一道题我们要注意它的性质:单调性。不难发现,前后缀越短越好找到符合条件的。我们又可以发现,它只能删除一个子串,那么我们先去除本身的前后缀,然后假设端点为
#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[1000005],b[1000005],pw[1000005];
int l,r,n;
int hash1(int l,int r){
return a[r]-a[l-1]*pw[r-l+1];
}
int hash2(int l,int r){
return b[l]-b[r+1]*pw[r-l+1];
}
bool check(int x){
for(int i=l;i<=r-2*x+1;i++){
if(hash1(i,i+x-1)==hash2(r-x+1,r)) return 1;
}
return 0;
}
bool chick(int x){
for(int i=l+2*x-1;i<=r;i++){
if(hash1(l,l+x-1)==hash2(i-x+1,i)) return 1;
}
return 0;
}
signed main(){
string x;
cin>>x;
int n=x.size();
l=1,r=n;
x=" "+x;
while(l<=r){
if(x[l]==x[r]){
l++;
r--;
}
else{
break;
}
}
if(l>r){
cout<<n/2+1;
return 0;
}
else{
pw[0]=1;
for(int i=l;i<=r;i++){
a[i]=a[i-1]*13331+x[i];
}
for(int i=r;i>=l;i--){
b[i]=b[i+1]*13331+x[i];
}
for(int i=1;i<=n;i++){
pw[i]=pw[i-1]*13331;
}
int ll=0,rr=(r-l+1)/2;
while(ll<rr){
int mid=(ll+rr+1)/2;
if(check(mid)) ll=mid;
else rr=mid-1;
}
int q=ll;
ll=0,rr=(r-l+1)/2;
while(ll<rr){
int mid=(ll+rr+1)/2;
if(chick(mid)) ll=mid;
else rr=mid-1;
}
cout<<max(l+q-1,l+ll-1);
}
return 0;
}
P10474 [ICPC-Beijing 2011] Matrix 矩阵哈希:
这是从我写的这道题的题解迁来的,你们可以去那道题看我的题解。
思路:
一道很好的哈希题,关键在于怎么写。其实我们可以发现,哈希一般都是一维的,而现在发现竟然是二维的。很明显,我们需要将二维的矩阵转为一维。\
怎么转呢?我们先看一下样例,按表格来画像这样:\
\
很明显,虽然一整个矩阵哈希值不好求小矩阵的哈希值,但我们可以对于每一行来求一次,使得每一行都为单独的一个,经过计算可以得到如下的
code:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[1005][1005],pw[1000005];
int hashh(int id,int l,int r){
return a[id][r]-a[id][l-1]*pw[r-l+1];
}
map<int,int> f;
signed main(){
cin.tie(0)->sync_with_stdio(0);
cout.tie(0)->sync_with_stdio(0);
int n,m,A,B;
cin>>n>>m>>A>>B;
pw[0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
char x;
cin>>x;
a[i][j]=a[i][j-1]*13331+x;
pw[(i-1)*m+j]=pw[(i-1)*m+j-1]*13331;
}
}
for(int i=1;i<=m-B+1;i++){
int num=0;
for(int j=1;j<=A;j++){
num=num*pw[B]+hashh(j,i,i+B-1);
}
f[num]=1;
for(int j=A+1;j<=m;j++){
num=num-pw[(A-1)*B]*hashh(j-A,i,i+B-1);
num=num*pw[B]+hashh(j,i,i+B-1);
f[num]=1;
}
}
int q;
cin>>q;
while(q--){
int s=0;
for(int i=1;i<=A;i++){
for(int j=1;j<=B;j++){
char x;
cin>>x;
s=s*13331+x;
}
}
if(f[s]==1) cout<<"1\n";
else cout<<"0\n";
}
return 0;
}
相似的题目:P4398 [JSOI2008] Blue Mary的战役地图。
P3501 [POI 2010] ANT-Antisymmetry:
简单题,留做练习。
T628207 编码(数据已加强):
这是最杂的一道题,其实本质上是二分+线段树+哈希,这道题留做挑战题,难度在 S 组的 T3。
#include<bits/stdc++.h>
#define unsigned long long
#define ls(x)(x<<1)
#define rs(x)(x<<1|1)
using namespace std;
const int NA=1e6+5;
int n,m;
string s;
int pw[NA];
int sum[NA<<2];
void giup(int x,int l,int r){
sum[x]=sum[ls(x)]*pw[r-(l+r)/2]+sum[rs(x)];
return ;
}
void mt(int x,int l,int r){
if(l==r){
sum[x]=s[l];
return ;
}
int mid=(l+r)/2;
mt(ls(x),l,mid);
mt(rs(x),mid+1,r);
giup(x,l,r);
return ;
}
void mn(int q,char w,int x,int l,int r){
if(l==q){
sum[x]=w;
return ;
}
int mid=(l+r)/2;
if(q<=mid) mn(q,w,ls(x),l,mid);
if(q>mid) mn(q,w,rs(x),mid+1,r);
giup(x,l,r);
return ;
}
int ans(int ll,int rr,int x,int l,int r){
if(ll>rr) return 0;
if(ll<=l&&r<=rr) return sum[x];
int mid=(l+r)/2;
if(rr<=mid) return ans(ll,rr,ls(x),l,mid);
else if(ll>mid) return ans(ll,rr,rs(x),mid+1,r);
return ans(ll,rr,ls(x),l,mid)*ans(ll,rr,rs(x),mid+1,r);
}
void init(){
pw[0]=1;
for(int i=1;i<=n;i++){
pw[i]=pw[i-1]*13331;
}
return ;
}
int main(){
cin>>n>>m>>s;
s=" "+s;
init();
mt(1,1,n);
while(m--){
int op;
cin>>op;
if(op==1){
int x;
char y;
cin>>x>>y;
mn(x,y,1,1,n);
}
else{
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(r1-l1!=r2-l2){
cout<<"NO\n";
continue;
}
int l=0,r=r1-l1+1;
while(l+1<r){
int mid=(l+r)/2;
if(ans(l1,l1+mid-1,1,1,n)==ans(l2,l2+mid-1,1,1,n)) l=mid;
else r=mid;
}
if(ans(l1,l1+l-1,1,1,n)+ans(l1+l+1,r1,1,1,n)==ans(l2,l2+l-1,1,1,n)+ans(l2+l+1,r2,1,1,n)) cout<<"YES\n";
else cout<<"NO\n";
}
}
return 0;
}
后话:
如果觉得写的好的话请回复 Clyyds。