【字符串 hash】【模板】讲解+例一:HDU1711 Number Sequence 【求串b在串a中首次出现的位置】+例二 :POJ1200 Crazy Search【求串a中长度为n的唯一子串】

【字符串 hash】【模板】讲解 + 例题一:HDU1711 Number Sequence 【求串b在串a中首次出现的位置】+ 例题二 :POJ1200 Crazy Search【求串a中长度为n的唯一子串】


字符串 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中出现的次数
    • 把S2 Hash出来,在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];
}

以luogu P3370 为例

扫描二维码关注公众号,回复: 2666818 查看本文章

模板二:单模数

#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;
}

Many people like to solve hard puzzles some of which may lead them to madness. One such puzzle could be finding a hidden prime number in a given text. Such number could be the number of different substrings of a given size that exist in the text. As you soon will discover, you really need the help of a computer and a good algorithm to solve such a puzzle.
Your task is to write a program that given the size, N, of the substring, the number of different characters that may occur in the text, NC, and the text itself, determines the number of different substrings of size N that appear in the text.

As an example, consider N=3, NC=4 and the text “daababac”. The different substrings of size 3 that can be found in this text are: “daa”; “aab”; “aba”; “bab”; “bac”. Therefore, the answer should be 5.

Input
The first line of input consists of two numbers, N and NC, separated by exactly one space. This is followed by the text where the search takes place. You may assume that the maximum number of substrings formed by the possible set of characters does not exceed 16 Millions.

Output
The program should output just an integer corresponding to the number of different substrings of size N found in the given text.

Sample Input
3 4
daababac

Sample Output
5
Hint
Huge input,scanf is recommended.

题意:
n为出现的字符种类,m为要求的字串长度,求在串a中长度为m的串,总共有多少种。

思路:
将所有不同的字母从1~m编号【注意没有0!】,把字符串转化为一个“m进制数”作为hash值,并进行判重。

AC代码:

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

using namespace std;

const int maxn = 1.6e7 + 5;
int n, m, ans, no, len;
int seen[maxn];
char a[maxn];
map<char, int> id;

int main()
{
    while(~scanf("%d%d", &n, &m))
    {
        ans = 0;
        memset(seen, 0, sizeof(seen));
        id.clear();
        no = 1;
        scanf("%s", a + 1);
        len = strlen(a + 1);
        for(int i = 1; i <= len; i++)
        {
            if(id.find(a[i]) == id.end())
                id[a[i]] = no++;
        }
        for(int i = 1; i + n - 1 <= len; i++)
        {
            int cur = 0;
            for(int j = i; j <= i + n - 1; j++)
                cur = cur * (m + 1) + id[a[j]];
            if(!seen[cur])
            {
                ans++;
                seen[cur] = 1;
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/Floraqiu/article/details/81538009