1. 程式人生 > >hash與字串hash入門

hash與字串hash入門

一.hash的引入.

考慮一個問題,給定n( 1 n 1 0 5 1\leq n \leq10^5

)個範圍在 [ 1..1 0 9 ] [1..10^9] 內的數,要求快速查詢x是否在這n個數中.

直接桶就MLE了,但是沒關係我們可以直接上平衡樹,或者直接STL裡的set或者map就好了,甚至你可以給n個數排個序再二分查詢…

但是如果要求 O ( 1 ) O(1) 查詢上述演算法就都沒了…這個時候hash的優勢就體現出來了.


二.hash表.

hash表是啥?其實也是個桶,好吧其實桶就是一種最簡單的hash表.

我們考慮桶個實現原理,桶其實就是開了一個數組 h

[ x ] h[x] 表示 x x 是否存在.然後我們思考對於上面這個問題,發現開 1 0 9 10^9 個桶也太浪費了,平均要 1 0 4 10^4 個位置才有一個數.

所以我們考慮讓設一個函式 f ( x ) f(x) ,考慮讓 h [ f ( x ) ] h[f(x)] 存x,然後讓 f ( x ) f(x) 的值域變得比較小,就可以省下很多記憶體了.

現在考慮如何設定 f ( x ) f(x) ,最簡單的當然是讓 f ( x ) = x   m o d   M f(x)=x \, mod\, M ,其中M是任意一個常數.不過事實證明當M為一個質數時就很少會出現 f ( x ) f(x) 衝突的情況了,所以M一般取一個n的4~5倍的大質數.

但是衝突出現得再少照樣會有,所以我們考慮如何處理衝突,一般有兩種方案:
1.給每一個位置存一個連結串列,表示 f ( x ) f(x) 為當前位置的所有數.
2.當插入 x x 衝突時,就往 ( f ( x ) + k )   m o d   M (f(x)+k)\,mod\,M 的位置放(其中k是一個比較小的常數,事實證明k取3,5,7會比較優秀),如果還是衝突就嘗試 ( f ( x ) + 2 k )   m o d   M (f(x)+2k)\,mod\, M 放…直到沒有衝突.

一般情況下本人比較喜歡用第2種(第2中只要寫個迴圈就好了,第1種要寫連結串列誒),所以我們這裡只提供第2種寫法的插入與查詢:

struct Hash_table{
  int h[M+9],v[M+9];      //v[x]是存在第x個位置的數的真實值

  void add(int &a,const int &b){a+=b;if (a>=M) a-=M;}

  void insert(int x){      //插入一個數
    int t=x;
    for (x%=M;h[x]&&v[x]^t;add(x,5));
    ++h[x];v[x]=t;
  }

  bool find(int x){      //查詢一個數
    int t=x;
    for (x%=M;h[x]&&v[x]^t;add(x,5));
    return h[x]?1:0;
  }
  
}



三.字串hash.

我們考慮一些複雜的資訊,例如字串上的一些資訊給如何運用hash表來處理.

很容易想到我們可以把字串看成一個P進位制的數,第一個字元是最高位,第二個字元是第二高位…這樣每個字串我們都可以看成一個數字了,接下來我們設一個字串s對應的P進位制數為 g ( s ) g(s) .

但是這個數字太大怎麼辦?我們可以直接講這個數字對一個數取模,這個數通常可以取 2 64 2^{64} ,這樣就可以直接用unsigned long long來存減少取模運算了,而且這樣子通常不會被卡.在這個情況下可以讓P等於131或者13331,事實證明這兩個數產生衝突的情況較小.

再來考慮在這樣的字串hash的處理下,對於兩個串 s s t t ,如果我們知道 s + t s+t s s 對應的P進位制數 g ( s + t ) g(s+t) g ( s ) g(s) ,那麼t對應的P進位制數就可以這樣求:
g ( t ) = g ( s + t ) g ( s ) P t g(t)=g(s+t)-g(s)*P^{|t|}

那麼我們要求一個串 s s 的一個子串 s [ l . . r ] s[l..r] 對應的P進位制數就可以通過預處理字首和來搞了.

求一個串s[l…r]對應的P進位制數就可以這樣寫:

ULL Hash(ULL *a,int l,int r){return a[r]-a[l-1]*Pow[r-l+1];}      //其中a陣列是字串s對應的字首和



四.例題與程式碼.

這裡就只給一道字串hash的例題.

題目:BZOJ4779.

這道題只需要二分一下可行的長度len就可以用字串hash過啦(一開始我還天真的認為 O ( n m 2 ) O(nm^2) 能過…).

程式碼如下:

#include<bits/stdc++.h>
  using namespace std;

#define Abigail inline void
typedef long long LL;
typedef unsigned long long ULL;

char rc(){
  char c=getchar();
  while (c<'A'||c>'Z') c=getchar();
  return c;
}

const int N=500;
const ULL P=131,M=5000081;

ULL Pow[N+9];

ULL Hash(ULL *a,int l,int r){return a[r]-a[l-1]*Pow[r-l+1];}

struct Hash_table{
  int h[M+9];
  ULL v[M+9];
  
  void add(ULL &a,const ULL &b){a+=b;if (a>=M) a-=M;}
  
  bool find(ULL x){
  	ULL t=x;
  	for (x%=M;h[x]&&v[x]^t;add(x,5));
  	return h[x]?1:0;
  }
  
  void insert(ULL x){
  	ULL t=x;
  	for (x%=M;h[x]&&v[x]^t;add(x,5));
  	++h[x];v[x]=t;
  }
  
  void erase(ULL x){
  	ULL t=x;
  	for (x%=M;h[x]&&v[x]^t;add(x,5));
  	--h[x];
  	if (h[x]<0) h[x]=0;
  }
  
}h;

int n,m,ans;
ULL s1[N+9][N+9],s2[N+9][N+9];

bool check(int len){
  int r,flag;
  for (int l=1;l+len-1<=m;++l){
  	r=l+len-1;flag=0;
  	for (int i=1;i<=n;++i)
  	  h.insert(Hash(s1[i],l,r));
  	for (int i=1;i<=n;++i)
  	  if (h.find(Hash(s2[i],l,r))) flag=1;
  	for (int i=1;i<=n;++i)
  	  h.erase(Hash(s1[i],l,r));
  	if (!flag) return true;
  }
  return false;
}

Abigail into(){
  scanf("%d%d",&n,&m);
  for (int i=1;i<=n;++i)
  	for (int j=1;j<=m;++j)
  	  s1[i][j]=s1[i][j-1]*P+rc();
  for (int i=1;i<=n;++i)
  	for (int j=1;j<=m;++j)
  	  s2[i][j]=s2[i][j-1]*P+rc();
}

Abigail work(){
  Pow[0]=1;
  for (int i=1;i<=m;++i)
    Pow[i]=Pow[i-1]*P;
  int l=1,r=m;
  for (int mid=l+r>>1;l+1<r;mid=l+r>>1)
    check(mid)?r=mid:l=mid;
  ans=check(l)?l:r;
}

Abigail outo(){
  printf("%d\n",ans);
}

int main(){
  into();
  work();
  outo();
  return 0;
}