hash与字符串hash入门

版权声明:转载请注明原出处啦QAQ(虽然应该也没人转载): https://blog.csdn.net/hzk_cpp/article/details/86365188

一.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;
}

猜你喜欢

转载自blog.csdn.net/hzk_cpp/article/details/86365188