字符串Hash學習筆記

請勿將字符串Hash和哈希表搞混。雖然二者都是哈希算法,但實現原理和應用上有很大區別。php

如下默認字符串下標從1開始,用\(s[l,r]\)表示字符串\(s\)的第\(l\)到第\(r\)個字符組成的子串。node

概述

字符串Hash經常使用於各類字符串題目的部分分中。字符串Hash能夠在\(O(1)\)時間內完成判斷兩個字符串的子串是否相同。一般能夠用這個性質來優化暴力以達到騙分的目的。ios

1、什麼字符串Hash

字符串Hash其實本質上是一個公式:
\[Hash(s)=(\sum_{i=1}^{len}{s[i]\cdot b^{len-i}})mod\ m\]
其中\(b,m\)是常量。算法

是否是感受很像一個生成函數數據結構

那爲何要採用這樣一個長得像生成函數的式子呢?函數

參照哈希表,咱們知道若是兩個字符串的Hash值相同,那麼這兩個串大機率是相同的。優化

但事實上咱們經常須要截取一個字符串的子串。能夠發現,對於\(s[l,r]\)這個子串的Hash值
\[Hash(s[l,r])=(\sum_{i=l}^{r}{s[i]\cdot b^{r-i}})\mod m\]
考慮原串\(s\)的前綴和
\[Hash(s[1,r])=(\sum_{i=1}^{r}{s[i]\cdot b^{r-i}})\mod m\]
\[Hash(s[1,l-1])=(\sum_{i=1}^{l-1}{s[i]\cdot b^{l-i-1}})\mod m\]
因而能夠推出:\(Hash(s[l,r])=Hash(s[1,r])-Hash(s[1,l-1])\cdot b^{r-l+1}\)spa

固然這都是在模\(m\)意義下。code

因而對於原串記錄Hash前綴和,就能夠\(O(1)\)截取子串Hash值blog

2、字符串Hash的用處

字符串Hash是一種十分暴力的算法。但因爲它能\(O(1)\)判斷字符串是否相同,因此能夠騙取很多分甚至過掉一些字符串題。

接下來先介紹字符串Hash與其餘字符串算法的對比。

1.字符串匹配(KMP)

這個不用說了,枚舉起始點掃一遍\(O(n)\)解決,時間複雜度和KMP相同。

2.迴文串

考慮以同一個字符爲中心的迴文串的子串必定是迴文串,因此知足可二分性。

將字符串正着和倒着Hash一遍,若是一個串正着和倒着的Hash值相等則這個串是迴文串。枚舉每一個節點爲迴文中心,二分便可。

時間複雜度相比較manacher較劣,爲\(O(n\log n)\)發現過不了模板題。

關鍵代碼

ull num[22000000],num2[22000010];
ull find_hash(int l,int r)
{
    if(l<=r)
    return num[r]-num[l-1]*_base[r-l+1];
    return num2[r]-num2[l+1]*_base[l-r+1];
}


int l=0,r=min(i-1,len-i);
int len=0;
while(l<=r)
{
    int mid=(l+r)>>1;
    if(find_hash(i,i+mid)==find_hash(i,i-mid)) l=mid+1,len=mid;
    else r=mid-1;
}

3.LCP(最長公共前綴)

LCP也具備可二分性。對於\(s_1,s_2\),和兩個前綴長度\(j,i\)\(j<i\),其中\(i\)\(s1,s2\)的前綴,則\(j\)也是\(s1,s2\)的前綴。

因此能夠在\(O(\log n)\)時間求出兩個串的前綴。

例:後綴排序

仿照上述求lcp的方式,由於決定兩個字符串的大小的是他們lcp的後一個字符,因此用快排加二分求lcp便可作到\(O(n\log^2n)\)的時間複雜度。比SA多了一個\(\log\)

#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<cstring>
#define base 233
#define ull unsigned long long
using namespace std;
ull bases[1000010],hashs[1000010];
char str[1000010];
int n;
inline ull get(int l,int r){return hashs[r]-hashs[l-1]*bases[r-l+1];}
bool cmp(int l1,int l2)
{
    int l=-1,r=min(n-l1,n-l2);
    while(l<r)
    {
        int mid=(l+r+1)>>1;
        if(get(l1,l1+mid)==get(l2,l2+mid)) l=mid;
        else r=mid-1;
    }
    if(l>min(n-l1,n-l2)) return l1>l2;
    else return str[l1+l+1]<str[l2+l+1];
}
int a[1000010];
int main()
{
    scanf("%s",str+1);
    n=strlen(str+1);
    bases[0]=1;
    for(int i=1;i<=n;i++)
    {
        bases[i]=bases[i-1]*base;
        hashs[i]=hashs[i-1]*base+str[i];
        a[i]=i;
    }
    stable_sort(a+1,a+n+1,cmp);
    for(int i=1;i<=n;i++) printf("%d ",a[i]);
    return 0;
}

難道字符串Hash只能去其餘算法的題嗎?不!暴力有時不僅是騙分的!

1.求字符串的循環節

例:OKR-A Horrible Poem

題意:求一個字符串的子串的最短循環節。

能夠發現對於串\(s[l,r]\),若是\(x\)是子串的一個循環節,必有\((r-l+1)\mod x=0\)\(s[l,r-x]=s[l+x,r]\)

若是存在長度\(y\)\(s\)的循環節(\(y\)\(x\)的因數)且\(x\)是串長的約數,則\(x\)必然是\(s\)的循環節。

考慮篩出每一個數的最大質因數,而後\(O(\log n)\)分解質因數,而後從大到小試除,看餘下的長度是不是循環節,若是是則更新答案。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#define N 500010
#define base 233
#define ull unsigned long long
using namespace std;
char str[N];
int len;
ull hashs[N],bases[N];
void make_hash(void)
{
    bases[0]=1;
    for(int i=1;i<=len;i++)
    {
        hashs[i]=hashs[i-1]*base+str[i]-'a'+1;
        bases[i]=bases[i-1]*base;
    }
}
ull get_hash(int l,int r){return hashs[r]-hashs[l-1]*bases[r-l+1];}
int prime[N],nxt[N],cnt;
int num[N],tot;
int main()
{
    scanf("%d",&len);
    scanf("%s",str+1);
    make_hash();
    for(int i=2;i<=len;i++)
    {
        if(!nxt[i]) nxt[i]=prime[++cnt]=i;
        for(int j=1;j<=cnt && i*prime[j]<=len;j++)
        {
            nxt[i*prime[j]]=prime[j];
            if(i%prime[j]==0) break;
        }
    }
    int m;
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        int lens=r-l+1;
        int ans=0;
        tot=0;
        while(lens>1)
        {
            num[++tot]=nxt[lens];
            lens/=nxt[lens];
        }
        lens=r-l+1;
        for(int j=1;j<=tot;j++)
        {
            int len1=lens/num[j];
            if(get_hash(l,r-len1)==get_hash(l+len1,r)) lens=len1;
        }
        printf("%d\n",lens);
    }
    return 0;
}

2.動態字符串查詢

如今的大多數字符串算法(好比KMP,AC自動機,SAM,SA,PAM)大多都是靜態的查詢或者只容許在最後插入。但若是要求在字符串中間插入或修改,這些算法就無能爲力了。

而字符串Hash的式子實際上是能夠合併的,只要知道左區間的Hash值,右區間的Hash值,和右區間的大小,就能夠知道總區間的Hash值。這就使得字符串Hash能夠套上不少數據結構來維護。

例1:火星人

題意:求兩個後綴的lcp,動態插入字符和改字符。

用平衡樹維護區間的Hash值,仿照上述求lcp的方法,時間複雜度\(O(n\log^2n)\)

因爲長度過長,只放主程序和關鍵代碼。

inline void update(int u)
{
    siz[u]=siz[ch[u][0]]+siz[ch[u][1]]+1;
    sum[u]=sum[ch[u][0]]*bases[siz[ch[u][1]]+1]+val[u]*bases[siz[ch[u][1]]]+sum[ch[u][1]];
}
int main()
{
    srand(19260817);
    scanf("%s",str+1);
    int m;
    scanf("%d",&m);
    n=strlen(str+1);
    bases[0]=1;
    for(int i=1;i<=100000;i++) bases[i]=bases[i-1]*base;
    for(int i=1;i<=n;i++) root=t.merge(root,t.new_node(str[i]-'a'+1));
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%s%d",opt,&x);
        if(opt[0]=='Q')
        {
            scanf("%d",&y);
            printf("%d\n",lcp(x,y));
        }
        else if(opt[0]=='R')
        {
            scanf("%s",opt);
            t.erase(root,x);
            t.insert(root,x-1,opt[0]-'a'+1);
        }
        else if(opt[0]=='I')
        {
            scanf("%s",opt);
            t.insert(root,x,opt[0]-'a'+1);
            n++;
        }
    }
    return 0;
}

例2:一道口胡的題

題意:維護一個字符串的子串的集合,一開始字符串和集合均爲空。

要求完成:在集合中插入一個子串,在字符串末尾加一個字符,求集合中與當前詢問的子串lcp最大值。

好比字符串爲\(abbabba\)

集合中的子串爲\(s[1,4],s[3,6],s[5,7]\)

此時查詢與子串\(s[2,5]\),答案爲2(\(s[2,5]\)\(s[5,7]\)的lcp爲2)。

\(m\leq 10^5\),強制在線(爲了防止SA過而特地加的)。

(假如存在SAM的作法能夠在下方評論)

首先,考慮一些暴力的作法:

  1. 暴力將一個子串和集合中的串用上述方法求lcp,時間複雜度\(O(m^2\log m)\)
  2. 暴力建trie,將子串掛到trie上,時間複雜度\(O(m^2)\),空間\(O(m^2)\)

顯然上述的方法都不可行。

考慮使用SA的想法,與一個串lcp最大的串必定是字典序最靠近它的串,也就是比它字典序大中最小的,和比它小中最大的。

仿照這個思路,使用上述比較兩個串字典序大小的方法,考慮使用平衡樹來維護子串集合中字典序的順序,查詢時只需查詢前驅後繼中的lcp最大值便可。

時間複雜度\(O(m\log^2m)\)

3、字符串Hash的缺點

雖然字符串Hash在暴力方面有極大的優點,但從它的名字中也能夠看出它存在的缺點:Hash衝突。

Hash Killer II

題意大概就是卡單Hash的代碼,且\(mod=10^9+7\)

根據生日悖論,對於\(n\)個不一樣的字符串,假如你的\(mod\)很小(好比\(10^9+7\)),極有可能把其中兩個判成相同。對於\(mod=N\)時錯誤的機率\(P\),有(詳見百度百科

能夠發現這個機率遠大於\(n/N\)

因此假如用天然溢出可能會被卡的狀況下,建議寫雙Hash。可是須要注意一點,雙Hash的常數十分之大。

相關文章
相關標籤/搜索