Hash算法详解

· · 个人记录

Hash算法是一个著名而简单的算法,它可以用一点空间 \text O(1) 标记一个大整数或以 \text O(len) 标记一个字符串.

Hash函数

Hash函数是将一个大数字或字符串转化成小数字塞进Hash表的,对字符串能做到基本不重复。

Hash表(散列表)

Hash表,又称散列表,能用桶的思想将一些数 \text O(1) 标记。

我们来想想,桶是不是占用空间很大,但时间复杂度很低。

我们来尝试对其进行改进。

我们可以选择一个模数m,在将要存进去的数 \mod m 再存。

于是我们设 Hash(x)=x \mod m

这样就可以解决空间问题,那么这样后,准确度这样就不是100%了。

于是对于每个桶,我们建一个邻接表,出现一个数把它插入那个邻接表,这样,遍历每个邻接表就能去重,访问。

.

至于m建议去一个较大的质数,如:999983.

代码:

#include<bits/stdc++.h>
using namespace std;
#define m 999983
struct hash{
    int head[m+2],val[m+2],nxt[m+2],tot;
    hash(){
        memset(head,0,sizeof head);
        memset(val,0,sizeof val);
        memset(nxt,0,sizeof nxt);
        tot=0;
    }
    void add(int x){
        int mod=x%m;
        val[++tot]=x;
        nxt[tot]=head[mod];
        head[mod]=tot;
    }
    void insert(int x){
        int mod=x%m;
        for(int i=head[mod];i;i=nxt[i])
            if(val[i]==x) return;
        add(x);
    }
    bool count(int x){
        int mod=x%m;
        for(int i=head[mod];i;i=nxt[i])
            if(val[i]==x) return 1;
        return 0;
    }
int main() {

    return 0;
}

练习:[JLOI2011] 不重复数字.

字符串Hash

基本用途

Hash的精髓来了,字符串Hash.

我们可以将一个字符串映射成一个整数存进Hash,已来去重.

那怎么转换呢,我们以进制转换思想,把字符串看成131进制,边取模边转换,这样,就可以了。

例题:【模板】字符串哈希.

简单的Hash去重.

代码:

#include<bits/stdc++.h>
using namespace std;
#define mod 999983
int ans;
vector<string>h[mod+2];
void insert(string x){
    int m=0;
    for(int i=0;i<x.size();i++) m=(m*131+x[i])%mod;
    for(int i=0;i<h[m].size();i++)
        if(h[m][i]==x) return;
    h[m].push_back(x);
    ans++;
}
int main() {
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        string s;
        cin>>s;
        insert(s);
    }
    cout<<ans;
    return 0;
}

练习:[USACO16DEC] Cities and States S.

接着,头脑风暴即将到来。

真正的 字符串Hash->随机算法 即将到来。

我们可以用某数值代表一个字符串,这样字符串冲突的概率极低,于是,用自然溢出的方法,用unsigned intunsigned long long存下一个字符串的值,不用取模(更快)。

但,不保证有两个不同字符串取模后的Hash值相同,所以它是随机算法。

我们可以求出那个字符串的前缀Hash值以用前缀和思想求出任意子串的Hash值。

一个子串[l,r]的Hash值为hash[r]-hash[l-1]*pow(r-l+1)

还可以求出那个字符串的后缀Hash值以用前缀和思想求出任意子串翻转后的的Hash值。

例题:【模板】manacher 算法.

最长回文子串,这在 O(n) 时间复杂度有点难。

于是,我们可以处理出前缀与后缀Hash值,在字符串每个字母的空隙插入其他字符(处理对称点在缝隙里的情况),对某个点进行左右扩散,如果它在扩散范围内的左串的Hash值与在扩散范围内的右串翻转后的Hash值一样(回文了),就记录ans,并继续扩散。

并且没越界。

因为以前搜出的范围长度小的子串就不用搜了(不会更新ans)。

所以当扩散时可以直接从长度为ans的长度枚举.

总时间为 \text O(n).

代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn=2.2e7+10;
unsigned int s1[maxn],s2[maxn],power[maxn];
int ans,tot,len;
char s[maxn],t[maxn];
unsigned int gethash1(int l,int r){
    return s1[r]-s1[l-1]*power[r-l+1];
}
unsigned int gethash2(int l,int r){
    return s2[l]-s2[r+1]*power[r-l+1];
}
int main() {
    scanf("%s",t);
    len=strlen(t);
    power[0]=1;
    s[++tot]='~';
    for(int i=0;i<len;i++)
        s[++tot]=t[i],s[++tot]='~';
    len=tot;
    for(int i=1;i<=len;i++){
        power[i]=power[i-1]*131;
        s1[i]=s1[i-1]*131+s[i]-'a'+1;
    }
    for(int i=len;i>=1;i--)
        s2[i]=s2[i+1]*131+s[i]-'a'+1;
    for(int i=1;i<=len;i++)
        while(i-ans>=1 && i+ans<=len && gethash1(i-ans,i+ans)==gethash2(i-ans,i+ans))
            ans++;
    cout<<ans-1;
    return 0;
}

提高精确度

我们可以对它计算以多个数为进制数进行双重保险,如果两个Hash值都相等那就相等,否则不相等(即便有一个Hash值相等)。

字符串Hash可以处理 KMP字符串匹配 类似的问题,以 \text O(n) 完成。

总结

Hash表是个很好的东西,可以以一点空间以 \text O(1) 解决去重问题。

连字符串都能去重。

字符串Hash是一个随机算法,它可以快速判断一些关于子串与字符串相等的问题。

当然,Hash还有很多用途可以发掘,如:快速访问\text O(1)的离散化,配二分用 \text O(n \log^2 n) 的时间复杂度求后缀数组。