HASH 字符串哈希 映射转化

哈希HASH的本质思想相似于映射、离散化。

哈希,经过给不一样字符赋不一样的值、而且钦定一个进制K和模数,从而实现一个字符串到一个模意义下的K进制数上。

它的主要目的是判重,用于$DFS$、$BFS$判重(八数码),字符串判断相等、出现等等。ios

本篇总结字符串哈希以及一些应用例题。数组

为什要用字符串哈希?

由于取出一个字符串是$O(n)$的,比较一遍又是$O(n)$的,何况要比较两个甚至多个。这就成了$n^2$级别的了。ide

那咱们比较数字怎么就不用这么麻烦呢?由于数字能够直接比较,(虽然不知道内部是怎么实现的,反正比一位一位比较确定快)因此咱们考虑把字符串映射到数字上。spa

就有了字符串哈希。code

经过字符串哈希,只要题目支持预处理,咱们能够$O(n)$预处理以后,$O(1)$进行提取,$O(1)$进行判重。blog

 

字符串哈希须要什么?

1.字符。初始坐标无所谓。排序

2.K进制数,一般选择$131$,$13331$,这两个质数冲突概率很小(不要问我为何)字符串

3.取模数,我用过 $1e9+7$,$998244353$,用$2^{64}$也能够,这里利用天然溢出,通常不会有问题。提一句,$unsigned\space long\space long$作减法,即便算出来应该是负数,会自动加上$2^{64}$,至关于$(a+mod-b)%mod$了。没有问题。string

 

处理hash:

1.预处理$K^{len}$ 放入$k[]$中储存。hash

2.顺便处理$hash[i]=hash[i-1]*K+str[i]$

 

hash的容器:

1.一个题可能产生不少哈希值。有的时候咱们要找一个容器存储。可以比较快速地查询一个$hash$值有没有出现过。

2.比较经常使用的是$map<ll,bool>$,由于自己map就是映射。

3.可是$map$不但有$logn$,常数也不小。因而就有了hash表。

其实就是对$hash$值再分类存放。就能够避免不少没有意义的查询。

再找一个模数,通常是全部哈希值出现次数的几分之一(数组能开下),能够的话,就取出现次数也行。

而后,哈希值先除以模数,余数就是位置。而后用邻接表存储。

 

字符串哈希的基本操做:

1.提取:$a[l,r]$段:$hash[r]-hash[l-1]*k[r-l]$ 相似前缀和。

2.插入,同处理。

操做均是$O(1)$

 

字符串哈希支持的应用操做:

1.判断字符串是否相等。取hash段比较便可,$O(1)$

2.找某两个位置开始的$LCP$(最长公共前缀),二分位置+$hash$判断 $O(logn)$ (长度够小,可用$trie$树,更好的支持多串$LCP$)(固然,若是你会$SA$,这些都是小儿科~)

3.判断两个串字典序大小,找$LCP$,判断下一位大小。$O(logn)$

4.找回文串。可是要正反二分。若是能够预处理的话,固然不如$manacher$。或者你用SA建反串而后找LCP。

哈希冲突

1.因为取模,因此有必定概率,两个不一样的串,可是哈希值相同。

咱们认为哈希值相同,串就相同了。因此,就会出现错误。

像1e9+7,unsigned long long 这些,均可以特殊构造卡掉。

见bzoj HASH KILLER系列。

2.解决方法:

①取大质数做为模数。$10^{15}$以上的模数更不容易被卡。

②双哈希

即处理两个哈希值。相同的字符串必定两个都相同,由于都是一样的构造方法。

若是哈希值不一样,必定是不一样的字符串。

这个时候,若是两个串的两个哈希值对应相等,咱们就认为相等。不然不等。

这样子冲突的几率就很小了。$1e9+7$,$998244353$的双模数就基本卡不掉了。

 

字符串哈希例题:

T1:POJ2758

给定一个字符串,要求维护两种操做
在字符串中插入一个字符
询问某两个位置开始的LCP
插入操做<=200,字符串长度<=5w,查询操做<=2w

分析:有人用后缀数组??不会。Splay??不会。

操做小于等于200,直接暴力重构是正解!!

注意:

1.插入字符位置可能远大于len,要向len+1取min

2.询问位置是初始位置,重构的时候,能够暴力循环记录每个初始位置如今已经变到了第几个位置。

#include<cstdio> #include<cstdlib> #include<algorithm> #include<iostream> #include<cmath> #include<cstring>
using namespace std; typedef long long ll; const int N=80000+210; const int mod=998244353; const int K=13331; ll h[N]; ll c[N]; int n,m; int len; int f[N]; int ne[N]; char o[N],a[N]; int main() { scanf("%s",o+1); n=strlen(o+1); memcpy(a+1,o+1,sizeof o);len=n; //cout<<" lenn "<<len<<endl;
    scanf("%d",&m); for(int i=1;i<=n;i++) ne[i]=i; c[0]=1; for(int i=1;i<=n+m+1;i++) { c[i]=(c[i-1]*K)%mod; if(i<=n) h[i]=(h[i-1]*c[1]+(int)o[i])%mod; } char ch,op; int num,x,y; //cout<<"fir "<<a+1<<endl;
    while(m--){ scanf(" %c",&op); if(op=='Q'){ scanf("%d%d",&x,&y); x=ne[x],y=ne[y]; //cout<<x<<" and "<<y<<endl;
            if(a[x]!=a[y]){ printf("0\n");continue; } int ans; int l=0,r=min(len-x,len-y)+1; //cout<<" origin "<<l<<" "<<r<<endl;
            while(l<=r){ int mid=(l+r)>>1; int ed1=x+mid-1; int ed2=y+mid-1; ll ha1=(h[ed1]+mod-h[x-1]*c[mid]%mod)%mod; ll ha2=(h[ed2]+mod-h[y-1]*c[mid]%mod)%mod; //cout<<mid<<" hash "<<ha1<<" "<<ha2<<endl;
                if(ha1==ha2) { ans=mid,l=mid+1; } else{ r=mid-1; } } printf("%d\n",ans); } else{ scanf(" %c%d",&ch,&num); if(num>len) num=len+1; ///add(num);
            len++; for(int i=len;i>=num+1;i--) a[i]=a[i-1]; a[num]=ch; for(int i=num;i<=len;i++) h[i]=(h[i-1]*c[1]+(int)a[i])%mod; for(int i=n;i>=1;i--) { if(ne[i]>=num) ne[i]++;else break;} } //cout<<a+1<<endl;
 } return 0; }
POJ2758

 

 

如下是配赠福利

树哈希:

咱们知道,一棵无根树能够以任何一个点为根。两个树可能看过去形态不一样,可是可能存在固定两个树的根,而后对整个树从新编号,使得彻底相同。

求树的同构就是这样。

相似字符串同构,咱们也要适用哈希。

模板例题:

BZOJ 4337: BJOI2015 树的同构

50棵树,50个节点。求同构。

方法:
1.对于两个同构的树,存在固定两个树的根,而后对整个树从新编号,使得彻底相同。

因此,咱们能够对一个树,以每一个点为根,而后dfs一遍。

$dfs$的时候,处理子树的$hash$值。

$hash$的$base$值和第几个儿子有关。是各不相同的素数。

而后,对于一个子树,把全部的儿子$hash$值,排序,从小到大合并。

而后对于全部的$hash$值,$sort$一遍。

两个树相同,当且仅当全部的$N$个点的$hash$值对应相同。

咱们的$hash$值考虑了深度、每一个点节点个数。因此不容易冲突。

2.咱们之因此要以每一个点为根,而后$dfs$一遍,

是由于可能从新编号后根不知道是哪两个。

可是这样比较暴力。$N^3$

发现,对于一个无根树,重心最多两个。

对于两个同构的树,若是咱们把重心的搭配4种枚举一下,那么必然存在一种树的$hash$相同。

因此,能够对每一个树以重心扫两边便可。

$hash$的$base$,也能够考虑用欧拉序。

相关文章
相关标签/搜索