哈希、哈希表詳解及應用

前置概念

Key : 咱們提供的一個要進行哈希的數字c++

\(f(x)\):即爲哈希函數,將key扔到這個函數裏面,能夠獲得Value,最核心的構造哈希表的東西算法

Hash地址:hash出來的值在哈希表中的存儲位置數組

進入正題

字符串hash

例題1:【模板】KMP

現有T組數據,每次給定兩個字符串\(s1\text{和}s2\),求\(s1\text{在}s2\)中出現了幾回。數據結構

首先考慮的固然是KMP了(逃ide

可是因爲咱們講的是字符串hash,那就考慮怎麼用字符串hash求解;函數

考慮每次枚舉每個子串的hash值,可是複雜度.....\(O(nm)\)優化

因此介紹一個優化技巧:滾動hashspa

滾動hash

滾動hash的誕生就是爲了不在\(O(m)\)的時間複雜度內計算一個長度爲m的字符串的hash值:code

咱們選取兩個合適的互質常數(雖然不知道爲何互質)b和h,對於字符串c,咱們搞一個hash函數:blog

\(hash(c)=(c_1b^{m-1}+c_2b^{m-2}+.....+c_mb^0)mod h\)

這個hash函數的構造過程是以遞推實現的,設

\(hash(c,k)\)爲前k個字符構成的子串的hash值,有

\(hash(c,k)=hash(c,k-1)\times b+c_{k}\)

爲方便理解,設\(c="ABCD"\)\(A=1,B=2....\)

\(hash(c,2)=1\times b+2\)

\(hash(c,3)=1 \times b^2+2 \times b +3\)

\(hash(c,4)=1\times b^3+2 \times b^2+3\times b+4\)

對於c的子串\(c'=c_{k+1}c_{k+2}....c_{k+n}\),有:

\(hash(c')=hash(c,k+n)-hash(c,k)\times b^n\)

很像前綴和是否是?

也很像b進制轉十進制是否是?

某位老師說過,探究新知的最好方法就是特值代入法,因此若是你們拿上面的那個例子來稍微作一下運算,就能很好地理解滾動hash這個優化方法了。

舉個例子:

若是咱們想求上面那個例子的子串\("CD"\)的hash值,那麼根據這個公式,就是:

\(hash("CD")=hash(4)-hash(2)\times b^2\)

\(hash(2)\times b^2 = 1\times b^3+2\times b^2\)

因此,原式\(=3\times b+4\)

這很像咱們有一個b進制數1234要轉成十進制,而上面所作的就是把1234中的12給殺掉,只留下34,再轉成十進制就OK了

因此,若是咱們預處理出\(b^n\),就能夠作到在\(O(1)\)的時間複雜度內get到任意子串的hash值,因此上面那道例題的時間複雜度就成功地降到了\(O(n+m)\)

可是有些細心的同窗會發現,若是某兩個子串的hash值撞車了怎麼辦呢?那麼能夠考慮double_hash,也就是將一個hash值取模兩次,書本上說:能夠將h分別取\(10^9+7\)\(10^9+9\),由於他們是一對「孿生質數」,雖然我也不知道這是什麼意思

(提醒:要開成unsigned long long,聽說是爲了天然溢出,省去取模運算)

哈希表

大概就是這樣子一個東西。

那這個東西有什麼用呢?

假設咱們要將中國每一個人的身份證號映射到每一個人的

若是有一我的的身份證號xxxxxx19621011XXXX

這是一個18位數!!!!(難道你要弄一個數組存??)

通過計算,\(1390000000/10^4=13900\),即至少有13900人的身份證後四位是同樣的

因此咱們能夠將全部身份證後四位相同的人裝到一個桶裏面,這個桶的編號就是這我的身份證的後四位,這就是哈希表,主要目的就是爲了解決哈希衝突,即F(key)的數值發生重複的狀況。

如上面的那個身份證號,咱們能夠考慮:

故,哈希表就是將\(F(key)\)做爲key的哈希地址的一種數據結構。

哈希的某些方法

直接定址法 :地址集合 和 關鍵字集合大小相同

數字分析法 :根據須要hash的 關鍵字的特色選擇合適hash算法,儘可能尋找每一個關鍵字的 不一樣點

平方取中法:取關鍵字平方以後的中間極爲做爲哈希地址,一個數平方以後中間幾位數字與數的每一位都相關,取得位數由表長決定。好比:表長爲512,=2^9,能夠取平方以後中間9位二進制數做爲哈希地址。

摺疊法:關鍵字位數不少,並且關鍵字中每一位上的數字分佈大體均勻的時候,能夠採用摺疊法獲得哈希地址,

除留取餘法:除P取餘,能夠選P爲質數,或者不含有小於20的質因子的合數

隨機數法:一般關鍵字不等的時候採用此法構造哈希函數較恰當。

可是這些東西貌似都是形式上的,具體怎麼操做仍是得靠實現

哈希表的實現

聽課的同窗裏面有多少人寫過圖/最短路等算法呢?

圖的存儲有兩種方法:

  1. 鄰接矩陣

  2. 鄰接表

在這裏咱們用鄰接表來實現。

void add(int a,int b,int c){
    dt[cnt].from=a;
    dt[cnt].to=b;
    dt[cnt].value=c;
    dt[cnt].next=head[a];
    head[a]=cnt++;
}

這是鄰接表。

void add(int a,int b){
    dt[cnt].end=b;
    dt[cnt].next=head[a];
    head[a]=cnt++;
    
}

這是哈希表。

很像有木有???

在這裏\(a,b\)是咱們用double_hash取出來的,取兩個不一樣的模數,兩個\(F(key)\)決定一個字符串。

惟一不一樣的是head數組的下標是\(key1\)

其實要不要這麼作隨你。


若是咱們要遍歷一個哈希表?

一樣,

for(int i=head[x];i;i=dt[i].next){
    .......
}

跟遍歷鄰接表如出一轍。


hash表中hash函數的肯定

若是是一個數的話,上面講過。(好像用離散化就好了)

若是是一個字符串的話,用前面的滾動hash就能夠了。

分兩種狀況:

若是你不想用double_hash:

那你也不須要把\(key1\)做爲head的下標了。

那就直接unsigned ll亂搞吧,天然溢出

若是你要用double_hash:

那你須要把\(key1\)做爲head的下標。

這時候你不能ull了,,那就弄那個什麼孿生質數取模吧。

b記得開小一點,最好算一算。

例題2:圖書管理

圖書館要搞一個系統出來,支持兩種操做:

add(s):表示新加入一本書名爲s的書。

find(s):表示查詢是否存在一本書名爲s的書。

對於每一個find操做,輸出一行yes或no。書名與指令之間有空格隔開,書名可能有一大堆空格,對於相同字母但大小寫不一樣的書名,咱們認爲它是不一樣的。

【樣例輸入】

4

add Inside C#

find Effective Java

add Effective Java

fine Effective Java

【樣例輸出】

no

yes


【題目分析】

這題是哈希表的一個變式,判斷一個字符串是否已經出現

能夠用滾動hash搞哈希表,採用double_hash

僞代碼(不知道算不算):

void add(int a,int b){
    .....
}
int find(int a,int b){
    for(int i=head[a];i;i=next[i]){
        if(value[i]==b)true;
    }
    false;
}
int main(){
    while(n--){
        cin>>order;
        gets(s);
        for(i=0;i<len;i++){
            key1=(key1*b1+s[i])%mod1;
            key2=(key2*b2+s[i])%mod2;
        }
        if(add)add(key1,key2);
        else{
            if(find(key1,key2))yes;
                else no;
        }
    }
}

這題還算簡單。

例題3 [LuoguP3498&POI2010]Beads

Jbc買了一串車掛飾裝扮本身,上有n個數字。它想要把掛飾扔進發動機裏切成\(k\)串。若是有n mod k !=0,則最後一段小於k的能夠直接捨去。並且若是有子串\((1,2,3)\)\((3,2,1)\),Jbc就會認爲這兩個子串是同樣的。Jbc想要多樣的掛飾,因此Jbc想要找到一個合適的\(k\),使得它能獲得不一樣的子串最多。

例如:這一串掛飾是:\((1,1,1,2,2,2,3,3,3,1,2,3,3,1,2,2,1,3,3,2,1)\)
\(k=1\)的時候,咱們獲得3個不一樣的子串: $(1),(2),(3) $

\(k=2\)的時候,咱們獲得6個不一樣的子串: $(1,1),(1,2),(2,2),(3,3),(3,1),(2,3) $

\(k=3\)的時候,咱們獲得5個不一樣的子串: \((1,1,1),(2,2,2),(3,3,3),(1,2,3),(3,1,2)\)

\(k=4\)的時候,咱們獲得5個不一樣的子串: \((1,1,1,2),(2,2,3,3),(3,1,2,3),(3,1,2,2),(1,3,3,2)\)

【輸入格式】

第一行一個整數n,第二行接n個數字。

【輸出格式】

第一行2個正整數,表示能得到的最大不一樣子串個數以及能得到最大值的k的個數。第二行輸出全部的k。

【數據範圍】

\(n\le 200000\)

\(1\le a_i\le n\)

【樣例輸入】

21

1 1 1 2 2 2 3 3 3 1 2 3 3 1 2 2 1 3 3 2 1

【樣例輸出】

6 1

2


【題目分析】

考慮最暴力的方法:

枚舉k,枚舉每個子串,從前日後、從後往前各掃一遍。

因此咱們就碰到了和字符串hash同樣的問題:

枚舉每個數複雜度有點高啊啊啊啊啊

爲了不在\(O(k)\)的複雜度內枚舉每個子串,咱們採用滾動hash(好像跟前面引述滾動hash的時候有點像)

預處理出正着跑的hash值以及反着跑的hash值。

枚舉每個子串,將正的hash值和反的hash值乘起來。

而後再扔到set裏,由於咱們知道set的特性:若是set裏面有兩個相同的數就會自動刪除。

最後再弄一個小根堆,若是當前k可以得到當前最大值,就扔進小根堆裏,不然將這個小根堆清空,再扔k。

而後呢?

沒有而後了。

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
ull n,a[1010101],power[1010101];
ull hash[1010101],hashback[1010101],ans=0;
set<ull>ba;
priority_queue<ull,vector<ull>,greater<ull> >gas;
const ull b=1926;
ull dash(ull i){
    ba.clear();
    for(ull j=1;j+i-1<=n;j+=i){
        ull cas1=hash[j+i-1]-hash[j-1]*power[i];
        ull cas2=hashback[j]-hashback[j+i]*power[i];
        ba.insert(cas1*cas2);
    }
    return (ull)ba.size();
}
int main(){
    cin>>n;
    for(ull i=1;i<=n;i++){
        cin>>a[i];
    }
    power[0]=1;
    for(ull i=1;i<1000000;i++)
        power[i]=power[i-1]*b;
    for(ull i=1;i<=n;i++)
        hash[i]=hash[i-1]*b+a[i];
    for(ull i=n;i>=1;i--)
        hashback[i]=hashback[i+1]*b+a[i];
    /*
    for(ull i=1;i<=n;i++)
        cout<<hash[i]<<" ";
    cout<<endl;
    for(ull i=n;i;i--)
        cout<<hashback[i]<<" ";
    cout<<endl;
    cout<<hash[3]-hash[1]*power[2]<<" "<<b*b+b+1<<endl;
    cout<<hashback[n-2]-hashback[n+1]*power[3]<<endl;*/
    
    for(ull i=1;i<=n;i++){
        ull cnt=dash(i);
        if(cnt>ans){
            ans=cnt;
            while(!gas.empty())gas.pop();
        }
        if(cnt==ans)gas.push(i);
    }
    cout<<ans<<" "<<gas.size()<<endl;
    for(;!gas.empty();){
        cout<<gas.top()<<" ";
        gas.pop();
    }
    
}

講完了

祝你們身體健康

參考:信息學奧賽一本通 提升篇

相關文章
相關標籤/搜索