哈希、哈希表详解及应用

前置概念

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();
    }
    
}

讲完了

祝你们身体健康

参考:信息学奥赛一本通 提升篇

相关文章
相关标签/搜索