浅谈Hash的应用

来源:互联网 发布:非洲军事 知乎 编辑:程序博客网 时间:2024/06/02 18:44

和Trie(字典树)一样,Hash其实是一个字典,可以存放数值或者是字符串。还可以用接近O(1)的时间进行查询或插入操作。

哈希中最重要的无非就是Hash函数了。Hash函数H(x)相当是一个映射,把x映射到一个表里面,从而解决各种问题。

由一道最简单的例题引入:

例1:现有n(n<=10^5)个数a[i](a[i]<=10^9),输出每一个a[i]是第几次出现。强制在线。

样例输入:6 

                 1 2 1 1000008 1 5

样例输出:1 1 2 1 3 1

【方法1】 很显然,这一题可以用BST实现STL中的map,或者直接使用STL,时间复杂度为O(nlogn)。

假如n再大一点?使O(nlogn)的方法超时?

【方法2】如果a[i]比较小,我们就可以拿一个桶把数存起来。可a[i]比较大是会爆掉。那么,可不可以MOD一个大质数,使得“桶”可以存得下?

直接取余法:这是Hash函数的一种写法。对于一个整数,直接取模一个大质数。然后存起来。则H(x) = x MOD Prime。

而存起来的“桶”叫做哈希表



其实,Hash函数还有很多种写法,不过这是最常用的。对于字符串,则可以把整个字符串看成n进制的数。可是,对于字符串,还有一种写法更优。那就是ELFHash函数:

附上ELF函数的模板:

int ELFhash(char *key){    unsigned long h=0;    unsigned long x=0;    while(*key)    {        h=(h<<4)+(*key++);        if( (x=h & 0xF0000000L)!=0)        {        h^=(x>>24);        h&=~x;        }    }    return h % N;}


回到例1,按照上面的做法,会发现一个问题:如果H(x)相同但x不相等那怎么办?这就是所谓的“Hash冲突”。解决冲突有以下几种方法:

一,开散列法

哈希表的每一个元素存的是一个链表。如果出先了冲突,就查找链表里的元素。若链表里还找不到该元素,则插入该元素到链尾。所以我们可以得到以下的程序:

class HashTable{public: bool find(int x){int id = H(x);int len = hash[id].size();for(int i=0;i<len;i++)if(hash[id][i]==x) return true;//找到了 return false;//没找到 }void insert(int x){int id = H(x);int len = hash[id].size();for(int i=0;i<len;i++)if(hash[id][i]==x) return;//已有该元素 hash[id].push_back(x);//插入该元素 }private:int H(int x)//哈希函数 {return x % MOD;}vector<int> hash[MOD];//Hash-Table };


二,闭散列法

开散列法用的是链表,而闭散列法则不用链表。它解决冲突的方法是:“如果已经有了元素‘霸占’了原来的位置,那么我也可以‘霸占’另一个位置。换一个位置把它存起来。”那么,该怎样换一个位置呢?

很显然,这是不能随机化的。有以下的两种方法解决。

1,线性探测法

若H(x)的位置已经有了元素,则有偏移量H(x)+1,H(x)+2,……(注意处理边界),一直跳到该位置为空或者元素相等。

这样的话,如果元素的Hash值比较集中,上面的方法就会很慢。则可以改变一下偏移量,如H(x)+i,H(x)+2i等。但要注意,如果之前直接取余法取余的不是质数,就有可能会陷入死循环而得不到解。

2,二次探测法

若H(x)的位置已经有了元素,则偏移量为H(x)+0^2,H(x)+1^2,H(x)+2^2……,这样就会少很多冲突了。

二次探测法模板:

class HashTable{//二次探测法 public:inline bool find(int x)  //查找是否有该元素 {      int id = H(x);    int t = 0;    while(1)    {    t++;     if(hash[id]==x) return true;     if(!hash[id]) return false;     id = id + t*t;     if(id>=MOD) id -= MOD;    }}     inline void insert(int x)  //插入 {      int id = H(x);      int t = 0;    while(1)    {    t++;    if(hash[id]==x) return;    if(!hash[id]) break;    id = id + t*t;     if(id>=MOD) id -= MOD;    }    hash[id]=x;}private:int H(int x) { return x % MOD; }int hash[MOD];//Hash-Table };



对于例1:可以使用开散列法来做(也可以使用闭散列法),那么,链表里面存的就是一个结构体,保存两个数:一个是元素,另一个是该元素出现的次数,这样就可以在大约O(n)的时间复杂度里算出来答案,已达理论下界。


Hash还可以处理更多在数值上的问题。

例2:poj1840

给定一个方程:

a1x13+ a2x23+ a3x33+ a4x43+ a5x53=0 

其中-50<=ai<=50,且为整数

求对于所有的i,xi!=0并且xi为整数而且-50<=xi<=50的方案个数。


【做法1】 暴力枚举x1,x2,x3,x4,x5,统计个数。时间复杂度O(100^5)

【做法2】暴力枚举x1,x2,x3,x4,而x5可以通过计算出来。时间复杂度O(100^4)。

【做法3】暴力枚举x1,x2,x3。则 a4x43+ a5x5= 0-a1x13+ a2x23+ a3x33

我们可以统计出i=a4x43+ a5x53的i的方案数。

由于i有可能很大,我们就可以用哈希表把方案数和i存起来了。时间复杂度O(100^3)。

注意,i可能是负数,而i的最小值为-2*50*50*50*50,只需要加偏移量=min(i)即可。


#include <cstdio>#include <iostream>#include <cstring>#include <algorithm>#include <string>#include <cstdlib>#include <bitset>#include <fstream>#include <queue>#include <stack>#include <map>#include <set>#include <ctime>#include <deque>#include <vector>using namespace std;typedef long long LL;#define INF 0x3fffffff#define Maxn 55#define MOD 1000007#define Offset 250000000int a1,a2,a3,a4,a5;struct Node{int value,count;Node(int v,int c){value = v; count = c;}};vector<Node> hash[MOD];int find(int x){int id = x % MOD;int len = hash[id].size();for(int i=0;i<len;i++)if(hash[id][i].value==x) return hash[id][i].count;return 0;}int insert(int x){int id = x % MOD;int len = hash[id].size();for(int i=0;i<len;i++)if(hash[id][i].value==x) return (++hash[id][i].count);hash[id].push_back(Node(x,1));return 1;}int main(){scanf("%d%d%d%d%d",&a1,&a2,&a3,&a4,&a5);int val;for(int i=-50;i<=50;i++)  if(i!=0)for(int j=-50;j<=50;j++)  if(j!=0)  {      val = a4*i*i*i + a5*j*j*j;  val = val+Offset;  insert(val);  }int ans = 0;ans = 0;for(int i=-50;i<=50;i++)  if(i!=0)for(int j=-50;j<=50;j++)  if(j!=0)for(int k=-50;k<=50;k++)  if(k!=0)  {        val = a1*i*i*i + a2*j*j*j + a3*k*k*k;  val = -val+Offset;  ans += find(val);  }  printf("%d\n",ans);return 0;}

例3:poj1840

题目大意:给定n个点(n<=1000),求这n个点组成的正方形的个数。|坐标|<=20000(时限3.5s)

首先,我们考虑正方形的性质:所有角为90度,所有边相等。所以,如果确定了一条边,那么这个正方形的形状也就确定了。而对于方向,则有两种情况:


而且,知道了一条边的两个点,就可以算出另外两个点的坐标。

如图所示:


只需判断D点与A点是否存在即可。可以用map来做,只不过时间复杂度是O(n^2logn),非常危险。

所以需要Hash把点给存起来,再查找是否存在就可以了。


而每个正方形都记录了4次,则最终答案要除以4。

#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>#include <vector>using namespace std;#define Maxn 1010#define MOD 99991typedef long long LL;struct Point{int x,y;Point() {}Point(int a,int b){x = a; y = b;}};Point a[Maxn];int n,ans;int H(int x,int y){    return (x*x+y*y) % MOD;}vector<Point> hash[MOD]; bool find(int x,int y){      int id = H(x,y);    int len = hash[id].size();    for(int i=0;i<len;i++)        if(hash[id][i].x==x && hash[id][i].y==y) return true;    return false;}void insert(int x,int y){    int id = H(x,y);    int len = hash[id].size();    hash[id].push_back(Point(x,y));}int main(){while(scanf("%d",&n) && n){ans = 0;for(int i=0;i<MOD;i++) hash[i].clear();for(int i=1;i<=n;i++) {scanf("%d%d",&a[i].x,&a[i].y);insert(a[i].x,a[i].y);}for(int i=1;i<n;i++)for(int j=i+1;j<=n;j++){int xx,yy;xx = a[j].x - a[i].x;yy = a[j].y - a[i].y;if(find(a[j].x-yy,a[j].y+xx)&&find(a[i].x-yy,a[i].y+xx))ans++;if(find(a[j].x+yy,a[j].y-xx)&&find(a[i].x+yy,a[i].y-xx))ans++;}cout<<ans/4<<endl;}return 0;}



不过,Hash值更多的是处理字符串的问题。

例4:

基因测试

题目描述

现代的生物基因测试已经很流行了。现在要测试色盲的基因组。有N个色盲的人和N个非色盲的人参与测试。
基因组包含M位基因,编号1至M。每一位基因都可以用一个字符来表示,这个字符是‘A’、'C'、'G'、'T'四个字符之一。
例如: N = 3, M = 8。

色盲者1的8位基因组是: AATCCCAT
色盲者2的8位基因组是: ACTTGCAA
色盲者3的8位基因组是: GGTCGCAA
正常者1的8位基因组是: ACTCCCAG
正常者2的8位基因组是: ACTCGCAT
正常者3的8位基因组是: ACTTCCAT

通过认真观察研究,生物学家发现,有时候可能通过特定的连续几位基因,就能区分开是正常者还是色盲者。
例如上面的例子,不需要8位基因,只需要看其中连续的4位基因就可以判定是正常者还是色盲者,这4位基因编号分别是:
(第2、3、4、5)。也就是说,只需要看第2,3,4,5这四位连续的基因,就能判定该人是正常者还是色盲者。
比如:第2,3,4,5这四位基因如果是GTCG,那么该人一定是色盲者。

生物学家希望用最少的连续若干位基因就可以区别出正常者和色盲者,输出满足要求的连续基因的最少位数。

输入格式 1810.in

第一行,两个整数: N和M。 1 <= N <= 500, 3 <= M <= 500。
接下来有N行,每一行是一个长度为M的字符串,第i行表示第i位色盲者的基因组。
接下来又有N行,每一行是一个长度为M的字符串,第i行表示第i位正常者的基因组。

输出格式 1810.out

一个整数。

输入样例 1810.in
3 8
AATCCCAT
ACTTGCAA
GGTCGCAA
ACTCCCAG
ACTCGCAT
ACTTCCAT
输出样例 1810.out

4


【做法1】 很容易就想到大暴力。枚举区间[i,j],然后暴力判断是否合法,判断两个字符串是否不等是O(m)的,则判断n*n次的时间就是O(n^2*m)。则总的时间复杂度为O(n^2*m^3)。

【做法2】可以想到哈希。在判断两个字符串是否相等时可以把字符串哈希。把色盲者的n个Hash值存到表里,然后把正常人的Hash值在哈希表里查找即可。时间复杂度为四次方级别。

【做法3】因为,枚举区间时是顺着枚举的。则考虑可以把上一次的结果记住,这一次就可以避免重复计算了。具体细节这里略去。则时间可降低一维。为O(n^3)≈O(125000000),在极限数据时还会超时。

【做法4】

如果答案再大一点,就不那么好了。如果答案小一点,就不符合情况。那么,我们就可以二分答案!

二分答案len,再从小到大枚举起始点i,则区间变成了[i,len+i],每一次区间都会向右移一位。应该怎样快速的计算Hash值呢?


#include <cstdio>#include <iostream>#include <cstring>#include <algorithm>#include <string>#include <cstdlib>#include <vector>using namespace std;typedef long long LL;#define INF 0x3fffffff#define Maxn 510#define MOD 3000007LLint n,m,n2;short a[Maxn<<1][Maxn];LL h[Maxn<<1];int hash[MOD];bool pd(int i){bool flag;for(int j=0;j<n;j++)hash[h[j]] = i;flag = true;for(int j=n;j<n2;j++)if(hash[h[j]]==i){flag = false;break;}return flag;}bool check(int len){memset(h,0,sizeof(h));memset(hash,0,sizeof(hash));for(int i=0;i<len;i++)for(int j=0;j<n2;j++)h[j] = ( (h[j]*4) + a[j][i] ) % MOD;LL Pow = 1;for(int i=1;i<len;i++) Pow = (Pow*4LL) % MOD;if(pd(len-1)) return true;for(int i=len;i<m;i++){for(int j=0;j<n2;j++)h[j] = ( ( ( h[j] - ((0LL+a[j][i-len]) * Pow) % MOD + MOD) *4LL ) + a[j][i] ) % MOD;if(pd(i)) return true;}return false;}int main(){freopen("1810.in","r",stdin);freopen("1810.out","w",stdout);scanf("%d%d",&n,&m);n2 = n<<1;char ch;for(int i=0;i<n2;i++){getchar();for(int j=0;j<m;j++){ch = getchar();if(ch == 'C') a[i][j] = 1;if(ch == 'T') a[i][j] = 2;if(ch == 'G') a[i][j] = 3;}}int l = 1;int r = m;while(l+1<r){int mid = (l+r)>>1;if(check(mid)) r = mid;else l = mid;}printf("%d\n",r);return 0;}


2 0
原创粉丝点击