字符串 hash 讲解及模板

字符串 hash 讲解

1、Hash是什么

  • Hash就是一个像函数一样的东西,你放进去一个值,它给你输出来一个值。输出的值就是Hash值。一般Hash值会比原来的值更好储存(更小)或比较。
  • Hash函数具有抗碰撞性、单向性、雪崩效应等
  • 有单关键字Hash和多关键字Hash等

2、字符串Hash解读

  • 字符串Hash:把字符串转换成一个整数的函数;而且要尽量做到使字符串对应唯一的Hash值。Hash主要返回一个值,这个值应当包含对象的特征

  • 字符串Hash主要用于快速判断字符串是否相同

  • 主要思路
    选取恰当的进制,可以把字符串中的字符看成一个大数字中的每一位数字,不过比较字符串和比较 大数字的复杂度并没有什么区别(高精数的比较也是O(n)的),但只要把它对一个数取模,然后认为取模后的结果相等原数就相等,那么就可以在一定的错误率的基础上O(1)进行判断了。

  • 如何选取进制?

  • 首先不要把任意字符对应到数字0,比如假如把a对应到数字0,那么将不能只从Hash结果上区分ab和b(虽然可以额外判断字符串长度,但不把任意字符对应到数字0更加省事且没有任何副作用),因为00,0000,000000都对应数值0,失去了长度信息

  • 对于字符串而言我们可以把字符串看成一个26进制数

  • 如果包含其他特殊符号,可看成|S|进制数,其中|S|为字符集大小

  • 关于进制的选择实际上非常自由,大于所有字符对应的数字的最大值,不要含有模数的质因子(那还模什么),比如一个字符集是a到z的题目,选择27、233、19260817 都是可以的。

  • 如何选取模数

  • 绝大多数情况下,不要选择一个10^9级别的数,因为这样随机数据都会有Hash冲突,根据生日悖论,随便找上109‾‾‾‾√个串就有大概率出现至少一对Hash 值相等的串(参见BZOJ 3098 Hash Killer II)。

  • 稳妥的办法是选择两个10^9级别的质数,只有模这两个数都相等才判断相等,但常数略大,代码相对难写,目前暂时没有办法卡掉这种写法(除了卡时间让它超时)(参见BZOJ 3099 Hash Killer III)。

  • 如果能背过或在考场上找出一个10^18级别的质数(Miller-Rabin),也相对靠谱,主要用于前一种担心会超时,后一种担心被卡。

  • 偷懒的写法就是直接使用unsigned long long,【自然溢出 】,不手动进行取模,它溢出时会自动对2^64,进行取模,如果出题人比较良心,这种做法也不会被卡,但这个是完全可以卡的,卡的方法参见BZOJ 3097 Hash Killer I。

  • 冲突了如何解决?

  • 实在不行放大/换一个mod(一个合理的质数)就行

  • 如果还是不放心可以开mod个vector或者set进行冲突处理

  • 冲突还是严重?利用多关键字Hash

map< pair<ULL,ULL> , set<ULL> >
map< pair<ULL,ULL> ,vector<ULL> >

3、小结:

  • Hash用于判断相等是O(1)的,不仅是对于字符串,对于序列,其他对象也是如此

  • 字符串Hash非常好写,可以代替kmp,有时候可以代替SAM,SA,ACAM等字符串相关的高级数据结构,且效率和常数一般要优秀的多

  • 自然溢出unsigned long long会提高效率,但会需要map或者离散化进行后续处理复杂度会变成logn 模数最好取较大的质数

4、可以用hash处理的字符串问题:

1、kmp问题

  • 给两个字符串S1,S2,求S2是否是S1的子串,并求S2在S1中出现的次数
  • 把S2Hash出来,在S1里找所有长度为|S2|的子串,Hash比较。
  • 效率 O(|S1|)

2、AC自动机

  • 给N个单词串,和一个文章串,求每个单词串是否是文章串的子串,并求每个单词在文章中出现的次数。
  • 把每一个单词hash成整数,再把文章的每一个子串hash成整数,接下来只需要进行整数上的查找即可。
  • 复杂度:O(|A|2+|S|)
  • 用AC自动机可以做到O(|A|+|S|)的复杂度,|S|是单词串总长,|A|是文章长度

3、后缀数组

  • 给两个字符串S1,S2,求它们的最长公共子串的长度

  • 将S1的每一个子串都hash成一个整数,将S2的每一个子串都hash成一个整数两堆整数,相同的配对,并且找到所表示的字符串长度最大的即可。

  • 复杂度:O(|S1|2+|S2|2)

  • 用后缀数组可以优化到O(|S|log|S|)

4、马拉车

  • 给一个字符串S,求S的最长回文子串。
  • 先求子串长度位奇数的,再求偶数的。枚举回文子串的中心位置,然后二分子串的长度,直到找到一个该位置的最长回文子串,不断维护长度最大值即可。
  • 复杂度:O(|S|log|S|)
  • 用manacher可以做到O(|S|)的复杂度

5、扩展kmp

  • 给一个字符串S,求S的每个后缀与S的最长公共前缀
  • 枚举每一个后缀的起始位置,二分长度,求出每个后缀与S的最长公共前缀。
  • 复杂度:O(|S|log|S|)
  • 用extend-kmp可以做到O(|S|)的复杂度

模板一:自然溢出

typedef unsigned long long ULL;
typedef long long LL;

unordered_map<ULL, bool> mp;

const int mx = 1000005;
ULL H[mx], P[mx];
ULL PA = 131;//进制数
int str1[mx], str2[mx];

void Hash(int str[], int len)
{
    H[0] = len;
    P[0] = 1;
    for(int i = 1; i <= len; i++)
    {
        H[i] = H[i - 1] * PA + str[i] + 1;
        P[i] = P[i - 1] * PA;
    }
}

ULL code (int l, int r)//获取[L,R]的hash值
{
    return H[r] - H[l - 1] * P[r - l + 1];
}

模板二:单模数

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
ull mod=19260817;
ull hashs(char s[])
{
    int len=strlen(s);
    ull ans=0;
    for (int i=0;i<len;i++)
        ans=(ans*base+(ull)s[i])%mod;
    return ans;
}
main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
    {
        scanf("%s",s);
        a[i]=hashs(s);
    }
    sort(a+1,a+n+1);
    for (int i=2;i<=n;i++)
        if (a[i]!=a[i-1])
            ans++;
    printf("%d\n",ans);
}

模板三:双模数

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
struct data
{
    ull x,y;
}a[10010];
char s[10010];
int n,ans=1;
ull mod1=19260817;
ull mod2=19660813;
ull hash1(char s[])
{
    int len=strlen(s);
    ull ans=0;
    for (int i=0;i<len;i++)
        ans=(ans*base+(ull)s[i])%mod1;
    return ans;
}
ull hash2(char s[])
{
    int len=strlen(s);
    ull ans=0;
    for (int i=0;i<len;i++)
        ans=(ans*base+(ull)s[i])%mod2;
    return ans;
}
bool comp(data a,data b)
{
    return a.x<b.x;
}
main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
    {
        scanf("%s",s);
        a[i].x=hash1(s);
        a[i].y=hash2(s);
    }
    sort(a+1,a+n+1,comp);
    for (int i=2;i<=n;i++)
        if (a[i].x!=a[i-1].x || a[i-1].y!=a[i].y)
            ans++;
    printf("%d\n",ans);
}

模板四:只用一个10^18质数

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
ull mod=212370440130137957ll;
ull hashs(char s[])
{
    int len=strlen(s);
    ull ans=0;
    for (int i=0;i<len;i++)
        ans=(ans*base+(ull)s[i])%mod;
    return ans;
}
main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
    {
        scanf("%s",s);
        a[i]=hashs(s);
    }
    sort(a+1,a+n+1);
    for (int i=2;i<=n;i++)
        if (a[i]!=a[i-1])
            ans++;
    printf("%d\n",ans);
}

例题一:HDU 1711 Number Sequence
Given two sequences of numbers : a[1], a[2], … , a[N], and b[1], b[2], … , b[M] (1 <= M <= 10000, 1 <= N <= 1000000). Your task is to find a number K which make a[K] = b[1], a[K + 1] = b[2], … , a[K + M - 1] = b[M]. If there are more than one K exist, output the smallest one.

Input
The first line of input is a number T which indicate the number of cases. Each case contains three lines. The first line is two numbers N and M (1 <= M <= 10000, 1 <= N <= 1000000). The second line contains N integers which indicate a[1], a[2], … , a[N]. The third line contains M integers which indicate b[1], b[2], … , b[M]. All integers are in the range of [-1000000, 1000000].

Output
For each test case, you should output one line which only contain K described above. If no such K exists, output -1 instead.

Sample Input
2
13 5
1 2 1 2 3 1 2 3 1 3 2 1 2
1 2 3 1 3
13 5
1 2 1 2 3 1 2 3 1 3 2 1 2
1 2 3 2 1

Sample Output
6
-1

题意
给出串a,串b,问串b在串a中第一次出现的位置

思路:
求出b的哈希值,在串a中枚举串b长度的串,求哈希值,比较

AC代码

#include <iostream>
#include <stdio.h>
#include <string>
#include <string.h>

using namespace std;

typedef unsigned long long ULL;
typedef long long LL;

const int mx = 1000005;
ULL H[mx], P[mx];
ULL PA = 131;
int str1[mx], str2[mx];

void Hash(int str[], int len)
{
    H[0] = len;
    P[0] = 1;
    for(int i = 1; i <= len; i++)
    {
        H[i] = H[i - 1] * PA + str[i] + 1;
        P[i] = P[i - 1] * PA;
    }
}

ULL code (int l, int r)
{
    return H[r] - H[l - 1] * P[r - l + 1];
}

int main()
{
    int t, n, m;
    scanf("%d", &t);
    while(t--)
    {
        int flag = 0, cur;
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= n; i++)
            scanf("%d", &str1[i]);
        for(int i = 1; i <= m; i++)
            scanf("%d", &str2[i]);
        Hash(str2, m);
        ULL a2 = code(1, m), a1;
        Hash(str1, n);
        for(int i = 1; i + m - 1 <= n; i++)
        {
            a1 = code(i, i + m - 1);
            if(a1 == a2)
            {
                printf("%d\n", i);
                flag = 1;
                break ;
            }
        }
        if(flag == 0)
            printf("-1\n");
    }
    return 0;
}

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值