HASH 字符串哈希 映射轉化


哈希HASH的本質思想類似於映射、離散化。

哈希,通過給不同字符賦不同的值、並且欽定一個進制K和模數,從而實現一個字符串到一個模意義下的K進制數上。

它的主要目的是判重,用於$DFS$、$BFS$判重(八數碼),字符串判斷相等、出現等等。

本篇總結字符串哈希以及一些應用例題。

為什要用字符串哈希?

因為取出一個字符串是$O(n)$的,比較一遍又是$O(n)$的,況且要比較兩個甚至多個。這就成了$n^2$級別的了。

那我們比較數字怎么就不用這么麻煩呢?因為數字可以直接比較,(雖然不知道內部是怎么實現的,反正比一位一位比較肯定快)所以我們考慮把字符串映射到數字上。

就有了字符串哈希。

通過字符串哈希,只要題目支持預處理,我們可以$O(n)$預處理之后,$O(1)$進行提取,$O(1)$進行判重。

 

字符串哈希需要什么?

1.字符。初始坐標無所謂。

2.K進制數,通常選擇$131$,$13331$,這兩個質數沖突幾率很小(不要問我為什么)

3.取模數,我用過 $1e9+7$,$998244353$,用$2^{64}$也可以,這里利用自然溢出,一般不會有問題。提一句,$unsigned\space long\space long$做減法,即使算出來應該是負數,會自動加上$2^{64}$,相當於$(a+mod-b)%mod$了。沒有問題。

 

處理hash:

1.預處理$K^{len}$ 放入$k[]$中儲存。

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$,也可以考慮用歐拉序。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM