Key : 咱們提供的一個要進行哈希的數字c++
\(f(x)\):即爲哈希函數,將key扔到這個函數裏面,能夠獲得Value,最核心的構造哈希表的東西算法
Hash地址:hash出來的值在哈希表中的存儲位置數組
現有T組數據,每次給定兩個字符串\(s1\text{和}s2\),求\(s1\text{在}s2\)中出現了幾回。數據結構
首先考慮的固然是KMP了(逃ide
可是因爲咱們講的是字符串hash,那就考慮怎麼用字符串hash求解;函數
考慮每次枚舉每個子串的hash值,可是複雜度.....\(O(nm)\)優化
因此介紹一個優化技巧:滾動hashspa
滾動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的質因子的合數
隨機數法:一般關鍵字不等的時候採用此法構造哈希函數較恰當。
可是這些東西貌似都是形式上的,具體怎麼操做仍是得靠實現
聽課的同窗裏面有多少人寫過圖/最短路等算法呢?
圖的存儲有兩種方法:
鄰接矩陣
鄰接表
在這裏咱們用鄰接表來實現。
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就能夠了。
分兩種狀況:
那你也不須要把\(key1\)做爲head的下標了。
那就直接unsigned ll亂搞吧,天然溢出
那你須要把\(key1\)做爲head的下標。
這時候你不能ull了,,那就弄那個什麼孿生質數取模吧。
b記得開小一點,最好算一算。
圖書館要搞一個系統出來,支持兩種操做:
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; } } }
這題還算簡單。
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(); } }
參考:信息學奧賽一本通 提升篇