Hash

  • 引例

\(N\)个正整数,\(M\)个询问,询问数字是否在\(N\) 个正整数中

建立hashtable[10010],hashtable[x] = true表示出现过,在读入时进行预处理

N个正整数,M个询问,询问数字在N个正整数中出现的次数

预处理hashtable[x]++

上面的共同点:将输入的数作为数组下标对这个数的性质进行统计

但是当规模过大,就不行,或者输入的是字符串,也不能用下标直接表示。所以引入了hash

  • 散列(hash):将元素key通过一个函数H转为整数H(key),使得该整数为尽量唯一代表这个元素

  • 当key为整数时:可以直接把key作为数组下标,或者用map函数实现映射,也可以自己设计映射函数
    • h(key)=key%p,p是一个质数
    • h(key)=a*key+b
  • 当key不为整数时

比如用一个整数表示二维坐标\((x,y),0\le{x},y\le{range}\) ,则可以设计映射函数为\(H[p]=x*range+y\) ,那么在定义域内,两个顶点的\(H[p]\) 不会相同,可以用这个整数唯一表示这个坐标。

!!一定记住可以设计一个这样的映射函数来表示。然后写一个函数,来进行转换!!!

int H(int x, int y);

Hash 表

  • hash表又叫散列表,由hash函数和链表结构构成。

  • hash本质是建立key到index的映射。

  • 但是由于值域变简单,可能容易造成冲突,即不同的key经过hash之后映射到同一个index中。

  • 解决冲突:开散列

    • 以hash函数的值域建立邻接表。每个值是邻接表,然后把映射到相同值的不同的key都加在这条链表里面。
    //值域0到cnt-1
    vector<int> p[cnt];
    //p[i]是一个邻接表
  • 步骤
    • 计算hash函数的值
    • 定位到对应链表进行遍历比较
  • 例子:

在一个长度为\(N\)的随机整数序列\(A\)中统计每个数出现次数

  • 设计hash函数\(H(x)=x\%p+1\)\(p\)为最接近\(N\)的质数。所以值域缩小到了\(p\) , 那么定位到链表为\(H(A[i])\)

元组\(a_1,a_2,a_3,..,a_6\) 和元组\(a_2,a_3,...,a_6,a_1\) 是相等的。求\(N\)个元组中是否存在相同的元组。两个元组相同,当前仅当从某个角开始顺序针或者逆时针,能得到相同的元组

  • 对于两片形状相同的雪花,六个角长度之和,长度之积都相等,则其hash值也相等。
  • \(H(a_1,a_2,...,a_6)=(\sum{a+\prod{a}})\%P\)\(P\) 取最接近\(N\) 的质数为99991
  • 枚举每一个元组,计算hash函数值,定位到对应的链表。
    • 如果链表为空,说明现在还没有相同的元组,则执行插入(插入的是结点编号\(i\))
    • 如果不为空,说明存在hash相同的元组,但是hash值相同,不代表元组相同,还要进一步遍历到那个位置,然后判断这两个元组是否相同。
const int maxn = 100010;
const int p = 99991; //取较大的质数
int n, snow[maxn][6];
vector<int> h[p]; //hash表
int H(int a[]){  //计算hash值
    int sum = 0, mul = 1;
    for(int i=0;i<6;i++){
        sum = (sum + a[i]) % p;
        mul = (long long)mul * a[i] % p;
    }
    return (sum+mul)%p;
}
for(int i=0;i<n;i++){
    int val = H(snow[i]); //计算hash值,以hash值为邻接表的表头
    for(unsigned int j=0;j<h[val].size();j++){ //链表不为空,则存在hash值相同的,进一步判断
        int k = h[val][j];
        if(equal(snow[k], snow[i])){ //判断是否相同
            printf("Twin snowflakes found.\n");
            return 0;
        }
    }
    h[val].push_back(i); //链表为空,则插入
}
printf("No two snowflakes are alike.\n");

字符串hash

  • 字符串Hash:将一个任意长度的字符串映射为一个非负整数,冲突概率几乎为0

步骤:

  • 取一固定值P,把字符串看成P进制数,并分配一个大于0的数值,代表每个字符
    • 对于小写字母构成的字符串,令\(a=1,b=2,...,z=26\)
    • P一般取1331,13331
  • 取一固定值M,求出该P进制数对M的余数,作为该字符串的Hash值
    • \(M=2^{64}\) 即直接使用unsigned long long 类型存储hash值,溢出时会自动对\(2^{64}\) 取模
    • 避免低效的mod运算
  • 对字符串的各种操作,都可以对P进制数进行算术运算反映到Hash值上

操作

  • \(S="abc"\) , 则\(H(S)=1*P^2+2*P+3\)
    • 即:写个求hash值的函数
  • \(S\)的Hash值\(H(S)\), 在\(S\)末尾添加一个字符\(c\) ,则\(H(S+c)=(H(S)*P+value[c])\%M\)
    • 相当于把数左移一位,乘以\(P\) ,再加上字符的表示值,取模
  • 已知\(S,S+T\)的hash值,则\(H(T)=(H(S+T)-H(S)*P^{len(T)})\%M\)
    • 相当于把\(S\)左移\(len(T)\) ,后面补0,然后对齐以后做减法

可以在\(O(N)\)内预处理字符串的所有前缀hash值,从而在\(O(1)\)内查询任意子串hash值

设字符串为\(S\),区间为\([0,len-1]\),\(f[i]\) 表示\([0,i]\) 的hash值

  • unsigned long long f[maxn];

  • \(value[s[0]]=s[0]-"a"+1\)
  • \(f[0]=value[s[0]]\)
  • \(f[i]=f[i-1]*P+value[s[i]]\)
  • 可以得到任意区间[l,r]的hash值为: \(f[r]-f[l-1]*P^{len(r-l+1)}\)
  • 当两个区间的hash相等,则可以认为两个子串相同

含有26个小写字母的字符串s,每次选择两个区间,q个询问两个区间中的子串是否相同

\(1\le{len(s),q}\le{10^6}\)

const int maxn = 1e6+10;
char s[maxn];
unsigned long long f[maxn]; //保存hash值
unsigned long long p[maxn]; //预处理,保存p的幂次的值
int s,q,l1,r1,l2,r2;
int main(){
    cin>>s>>q;
    int len = strlen(s);
    f[0] = s[0]-'a'+1;
    p[0] = 1;
    for(int i=1;i<len;i++){
        f[i] = f[i-1]*131+s[i]-'a'+1;
        p[i] = p[i-1]*131; //131^i
    }
    for(int i=1;i<=q;i++){
        cin>>l1>>r1>>l2>>r2;
        if(f[r1]-f[l1-1]*p[r1-l1+1] == f[r2]-f[l2-1]*p[r2-l2+1]){
            puts("yes");
        }else{
            puts("no");
        }
    }
    
}

给出\(N\)个只有小写字母的字符串,求不同的字符串的个数

  • set<string> s;
  • map<string, int> mp;
  • typedef unsigned long long ull;
    ull hashfunc(string str){
        ull ans = 0;
        for(int i=0;i<str.length();i++){
            ans = ans * p + str[i]-'a';
        }
        return ans;
    }
    ans.push_back(hashfunc(str));
    //对于重复的字符串,进行排序,再统计!!!!!!!重复元素的统计方法!!!
    for(int i=0;i<ans.size();i++){
        if(i==0||ans[i]!=ans[i-1]) cnt++;
    }

输入两个长度不超过1000的字符串,求它们的最长公共子串的长度

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

typedef unsigned long long ull; 
const int maxn = 1e4+10;
char s1[maxn],s2[maxn];
ull p[maxn], f1[maxn],f2[maxn];
ull cal(int l, int r, ull f[]){ //返回hash值
    return f[r] - f[l-1]*p[r-l+1];
}
struct node{
    int l,r;
    ull val;
};
vector<node> pr1,pr2; //保存hash值,边界
bool cmp(node x, node y){ //排序
    int a = (x.r-x.l);
    int b = (y.r-y.l);
    if(a!=b) return a>b;
    if(x.r != y.r) return x.r < y.r;
    return x.l < y.l;
}
int main(){
    scanf("%s%s",s1,s2);
    int len1 = strlen(s1);
    int len2 = strlen(s2);
    //预处理
    p[0] = 1; f1[0] = s1[0]; f2[0] = s2[0];
    for(int i=1;i<len1;i++){
        f1[i] = f1[i-1]*131+s1[i];
        p[i] = p[i-1]*131;
    }
    for(int i=1;i<len2;i++){
        f2[i] = f2[i-1]*131+s2[i];
        p[i] = p[i-1]*131;
    }
    //保存两个字符串的所有子串的hash值
    for(int i=0;i<len1;i++){
        for(int j=i;j<len1;j++){
            node tmp = {i,j,cal(i,j,f1)};
            pr1.push_back(tmp);
        }
    }
    for(int i=0;i<len2;i++){
        for(int j=i;j<len2;j++){
            node tmp = {i,j,cal(i,j,f2)};
            pr2.push_back(tmp);
        }
    }
    //从大到小排序
    sort(pr1.begin(), pr1.end(), cmp);
    //遍历两个数组,如果值相同,就保存,并break
    int l,r;
    for(int i=0;i<pr1.size();i++){
        bool flag = false;
        for(int j=0;j<pr2.size();j++){
            if(pr1[i].val == pr2[j].val){
                l = pr1[i].l;
                r = pr1[i].r;
                flag = true;
                break;
            }
        }
        if(flag) break;
    }
    //输出答案
    for(int i=l;i<=r;i++){
        printf("%c",s1[i]);
    }
    printf("\n");
}
//算法笔记中的代码
typedef long long ll;
const int mod = 1e9+7;
const int p = 1e7+19;
ll hashfunc(string str){
    ll ans = 0;
    for(int i=0;i<str.length();i++){
        ans = (ans * p + str[i]-'a') % mod;
    }
}

转载于:https://www.cnblogs.com/doragd/p/11306633.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值