字符串哈希基础与应用

目录

 

字符串哈希

例题

A:POJ-3461 Oulipo

B:POJ-2406 Power Strings

C:POJ-2752 Seek the Name, Seek the Fame

D:HDU-1880 魔咒词典

E:POJ-1743 Musical Theme

F:SCU-4438 Censor

G:HDU-1280 前m大的数

H:HDU-1496 Equations


字符串哈希

一、引入

        哈希算法是通过一个哈希函数H,将一种数据(如字符串)转化为另一种数据(通常转化为整形数值),有些题可用map做,但数据一大就要用到字符串哈希

二、字符串哈希

        寻找长度为n的主串S中的匹配串T(长度为m)出现的位置或次数属于字符串匹配问题。朴素算法(或称为暴力)就是枚举所有子串的起始位置,每枚举一次就要使用O(m)的时间,总共要O(nm)的时间。当然字符串匹配可以用KMP做,但这里介绍一下字符串哈希。

        字符串哈希就是将每个字符串转化为一个数值,然后遍历主串,判断在主串起始位置为i长度为m的字符串的哈希值与匹配串的哈希值是否相等即可,每次判断为O(1)的时间。这样就可以转化为O(n)的时间完成判断。那么问题来了,怎么预处理哈希值呢?

        我们选用两个互质常数base和mod,假设匹配串T=abcdefg……z(注意这里不是指T只有26位)那么哈希值为  H(T)=(a*base^(m-1)+b*base^(m-2)+c*base^(m-3)+……+z)%mod。相当于把每个字符串转换为一个base进制数,所以对于每道题我们取base时,要大于每一位上的值(避免重复),例如我们用的十进制数每一位都是小于10的。

例如字符串C="ABDB",则H(C)=‘A’+'B'*base+'D'*base^2+'B'*base^3(本人习惯直接取字符askII码值,也可以使‘A’=1)

         那么怎么判断主串起始位置为i长度为m的字符串的哈希值与匹配串的哈希值是否相等呢?这里有个公式,若求字符串中第i位到第j位的哈希值(i<j),则这个值为H(j)-H(i-1)*base^(j-i+1)。有了这个公式,我们可以预处理一个数组H[i]表示字符串从第一位到第i位的哈希值和数组power[i]表示base^i。加上判断的时间,总时间为O(n+m)。

        在计算时,我们可以使用无符号类型(通常本人习惯使用unsigned long long)的自然溢出,这样就可以不用%mod,包括减法也方便许多。

        当然哈希会有可能重复,base值越大重复可能性越小,本人通常取131或233317。也可使用双哈希,即两个不同的mod
具体例子可看例题

例题

A:POJ-3461 Oulipo:这个题kmp里面写过,基础题,判断子串在模式串中出现的次数,简单的哈希处理下就行了,代码:

#include <iostream>
#include <cstring>
#include <cstdio>
 
using namespace std;
typedef unsigned long long ll;
const int base = 31;
const int maxn = 1000050;
 
char sub[maxn],str[maxn];
ll xp[maxn];
ll hash[maxn];
 
int main()
{
    int T,i;
    scanf("%d",&T);
 
    xp[0]=1;
    for(i=1;i<maxn;i++)
        xp[i]=xp[i-1]*base;
 
    while(T--)
    {
        memset(sub,0,sizeof(sub));
        memset(str,0,sizeof(str));
        scanf("%s",sub);
        scanf("%s",str);
        int L=strlen(sub);
        int n=strlen(str);
 
        ll sub_num=0;
        for(i=L-1;i>=0;i--)
        {
            sub_num=sub_num*base+(sub[i]-'a')+1;
        }
 
        hash[n]=0;
        for(i=n-1;i>=0;i--)
        {
            hash[i]=hash[i+1]*base+(str[i]-'a')+1;
        }
 
        int ans=0;
        for(i=0;i<=n-L;i++)     ///Caution!!! it is (i<=n-L) or (i<n-L+1)
        {
            if(sub_num==hash[i]-hash[i+L]*xp[L])
                ans++;
        }
        printf("%d\n",ans);
    }
    return 0;
}

B:POJ-2406 Power Strings:给定若干个长度的字符串,询问每个字符串最多是由多少个相同的子字符串重复连接而成的。如:ababab则最多有 3个 ab 连接而成。哈希做法即为枚举重复长度,然后判断即可,具体看代码:

#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
typedef unsigned long long ll;
const int maxn = 1e6 + 100;
const int temp = 31;
char s[maxn];
ll f[maxn], Hash[maxn];

int main() {
    f[0] = 1;
    for (int i = 1; i < maxn; i++)
        f[i] = f[i - 1] * temp;
    while (scanf("%s",s)) {
        if (s[0] == '.') break;
        int l = strlen(s);
        Hash[l] = 0;
        for (int i = l - 1; i >= 0; i--) {
            Hash[i] = Hash[i + 1] * temp + (s[i] - 'a') + 1;
        }
        int ans = 0;
        for (int i = 1; i <= l; i++) {
            if (l % i != 0) continue;
            ll ha = Hash[0] - Hash[i] * f[i];
            int k = 0;
            for (k = i; k < l; k = k + i) {
                if (ha != Hash[k] - Hash[k + i] * f[i]) break;
                else ha = Hash[k] - Hash[k + i] * f[i];
            }
            if (k == l) {
                ans = l / i;
                break;
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

C:POJ-2752 Seek the Name, Seek the Fame:题意:对于一个字符串s,找出所有相同的前缀后缀长度。思路:暴力枚举长度,然后判断前后缀值是否一样即可。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N=400005;
const int mod = (1 << 15) - 1;
const int seed=31;
ll Hash[N];
ll f[N];
char s[N];
int sl;
int main()
{
    while(~scanf("%s",s))
    {
        sl=strlen(s);
        f[0]=1;
        for(int i=1;i<=sl;i++) f[i]=f[i-1]*seed;
        Hash[0]=s[0]-'a';
        for(int i=1;i<sl;i++) Hash[i]=Hash[i-1]*seed+s[i]-'a';
        bool flag=true;
        for(int i=0;i<sl;i++)
        {
            if(Hash[i]==Hash[sl-1]-Hash[sl-2-i]*f[i+1])
            {
               if(flag)
               {
                   printf("%d",i+1);
                   flag=false;
               }
               else printf(" %d",i+1);
            }
        }
        printf("\n");
    }
    return 0;
}

D:HDU-1880 魔咒词典: 哈利波特在魔法学校的必修课之一就是学习魔咒。据说魔法世界有100000种不同的魔咒,哈利很难全部记住,但是为了对抗强敌,他必须在危急时刻能够调用任何一个需要的魔咒,所以他需要你的帮助。给你一部魔咒词典。当哈利听到一个魔咒时,你的程序必须告诉他那个魔咒的功能;当哈利需要某个功能但不知道该用什么魔咒时,你的程序要替他找到相应的魔咒。如果他要的魔咒不在词典中,就输出“what?”。

#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
const int maxn=1e5+10;
const int mod=1e9+7;
const int inf=1e8;
typedef long long ll;
using namespace std;
char str1[maxn][30],str2[maxn][100];
int len=0;
struct node
{
    int has,i;
    bool friend operator<(node a,node b)
    {
        return a.has<b.has;
    }
}cnt1[maxn],cnt2[maxn];///cnt1保存魔咒的hash值,cnt2保存对应功能的hash值
int gethash(char *str)///算一个字符串的hash值
{
    int sum=0,seed=131;
    int l=strlen(str);
    for(int i=0;i<l;i++)
        sum=sum*seed+str[i];
    return sum;
}
void solve()///将魔咒和对应功能的字符串转化成相应的hash值
{
    for(int i=0;i<len;i++)
    {
        cnt1[i].has=gethash(str1[i]);
        cnt2[i].has=gethash(str2[i]);
        cnt1[i].i=cnt2[i].i=i;
    }
    sort(cnt1,cnt1+len);///排序,方便后面二分查找。
    sort(cnt2,cnt2+len);
}
int main()
{
    while(scanf("%s",str1[len])&&strcmp(str1[len],"@END@"))
    {
        getchar();
        gets(str2[len++]);
    }
    solve();
    int n;
    scanf("%d",&n);
    getchar();
    for(int i=0;i<n;i++)
    {
        char str[105];
        gets(str);
        node temp;
        if(str[0]=='[')
        {
            temp.has=gethash(str);
            int pos=lower_bound(cnt1,cnt1+len,temp)-cnt1;///输入的魔咒,在对应数组里面找
            if(temp.has==cnt1[pos].has)
                printf("%s\n",str2[cnt1[pos].i]);///若有这个魔咒,输出对应的功能
            else
                printf("what?\n");
        }
        else
        {
            temp.has=gethash(str);
            int pos=lower_bound(cnt2,cnt2+len,temp)-cnt2;///跟上面原理一样
            if(temp.has==cnt2[pos].has)
            {
                int len=strlen(str1[cnt2[pos].i]);
                str1[cnt2[pos].i][len-1]='\0';///这步和下面的+1都是为了不输出那个中括号
                printf("%s\n",str1[cnt2[pos].i]+1);
            }
            else
                printf("what?\n");
        }
    }
    return 0;
}

E:POJ-1743 Musical Theme:该题题意是给定一个音乐串,要求最长的主题串满足:可以找到两个这样的串,在对方的每一位添加一个数字 。两个串互相不能够有重叠。

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
typedef unsigned long long ulint;
const ulint seed = 30007uLL;
#define maxn 20020
#define mod 100003
ulint H[maxn], xp[maxn];
int s[maxn], N;

void initHash()
{
    H[0] = s[0];
    for(int i = 1; i < N; i++)
        H[i] = H[i - 1]*seed + s[i];
}

ulint askHash(int l, int r)
{
    if(l == 0) return H[r];
    return H[r] - H[l - 1]*xp[r - l + 1];
}



ulint h[mod];
int bg[mod], nx[mod], pos[mod];

bool check(int len)
{
    memset(bg, 0, sizeof(bg));

    ulint ht;
    int e = 0;

    for(int i = 0, l, r; i + len - 1 < N; i++)
    {
        l = i, r = i + len - 1;
        ht = askHash(l, r);

        for(int p = bg[ht % mod]; p; p = nx[p])
            if(h[p] == ht && i - pos[p] >= len)
                return true;

        h[++e] = ht;           //这几个数组用法可以记住,当模板用
        nx[e] = bg[ht % mod];
        bg[ht % mod] = e;
        pos[e] = i;
    }

    return false;
}

int main()
{
    xp[0] = 1;
    for(int i = 1; i < maxn; i++)
    {
        xp[i] = xp[i-1] * seed;
    }

    while(scanf("%d", &N) && N)
    {
        for(int i = 0; i < N; i++)
        {
            scanf("%d", &s[i]);
        }
        N--;
        for(int i = 0; i < N; i++)
        {
            s[i] = 89 + s[i+1] - s[i];//+89防止出现负数
        }

        initHash();

        int ans = 0, l = 1, r = N / 2, m;
        while(l <= r)
        {
            m = (l + r) >> 1;

            if(check(m))
            {
                l = m + 1;
                ans = m;
            }
            else r = m - 1;
        }
        cout << ((ans >= 4)? ans+1: 0) << endl;  //>=4因为求的是间歇的个数,4个间歇相等即5个字符满足
    }
    return 0;
}

F:SCU-4438 Censor:给定一个字符串A和一个字符串B,如果如果B中存在A字符串,就在B中把A字符串去掉,输出最后去掉A字符串之后B字符串,注意aaabcbc可以去2次abc,结果为a。思路应该都有,没有看代码应该也能明白。代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N=5e6+10;
const int mod = (1 << 15) - 1;
const int seed=31;
ll Hash1,Hash2[N];
ll f[N];
char ans[N];
char s1[N],s2[N];
int len1,len2;
int main()
{
    while(~scanf("%s %s",s1,s2))
    {
        int tot=0;
        len1=strlen(s1);
        len2=strlen(s2);
        if(len1>len2){
            cout<<s2<<endl;
            continue;
        }
        Hash1=0;
        f[0]=1;
        for(int i=1;i<=len2;i++) f[i]=f[i-1]*seed;
        for(int i=0;i<len1;i++) Hash1=Hash1*seed+s1[i]-'a'+1;
        Hash2[0]=0;
        for(int i=0;i<len2;i++)
        {
            ans[tot++]=s2[i];
            Hash2[tot]=Hash2[tot-1]*seed+s2[i]-'a'+1;
            if(tot>=len1&&(Hash2[tot]-Hash2[tot-len1]*f[len1]==Hash1))
                tot-=len1;
        }
        for(int i=0;i<tot;i++)
            cout<<ans[i];
        cout<<endl;
    }
    return 0;
}

G:HDU-1280 前m大的数:给n个数,求前m大的数,每两个数可以两两相加:

#include<stdio.h>
#include<cstring>
int main()
{
    int hash[10010];
    int a[3010];
    int n,m;
    int i,j;
    while(scanf("%d %d",&n,&m)!=EOF)
    {
        memset(hash,0,sizeof(hash));
        for(i=0;i<n;i++)
        {
            scanf("%d",&a[i]);
        }
        int xx=0;
        for(i=0;i<n;i++)
        {
            for(j=0;j<i;j++)
            {
                hash[a[i]+a[j]]++;  //我也不明白这个解法为什么见哈希,可能数组名为hash(手动滑稽)
                if(a[i]+a[j]>xx)
                {
                    xx=a[i]+a[j];
                }
            }
        }
        for(i=xx;i>=0;i--)
        {
            while(hash[i]&&m>1)
            {
                printf("%d ",i);
                hash[i]--;
                m--;
            }
            if(m==1&&hash[i])
            {
                printf("%d\n",i);
                break;
            }
        }
    }
    return 0;
}

H:HDU-1496 Equations:题目大意:给定a,b,c,d。a*x1^2+b*x2^2+c*x3^2+d*x4^2=0,其中x1~x4 在 [-100,100]区间内, a,b,c,d在[-50,50] 区间内。求满足上面那个式子的所有解的个数。思路:将等式变形为a*x1^2+b*x2^2= -(c*x3^2+d*x4^2) 先用两重循环列举a,b的所有情况,将等式的左边结果存入hash表。再用两重循环列举c,d的所有情况,看看结果的相反数在不在hash表中。统计输出。

#include <iostream>
#include <stdio.h>
#include <string.h>
 
using namespace std;
 
const int N = 100;
const int N2 = N * N * N;
int sum1[N2 + 1], sum2[N2 + 1];
 
int main()
{
    int a, b, c, d;
    while(~scanf("%d%d%d%d", &a, &b, &c, &d)) {
        if((a > 0 && b > 0 && c > 0 && d > 0) || (a < 0 && b < 0 && c < 0 && d < 0)) {
            printf("0\n");
            continue;
        }
 
        memset(sum1, 0, sizeof(sum1));
        memset(sum2, 0, sizeof(sum2));
 
        int sum = 0;
        for(int i = 1; i <= N; i++)
            for(int j = 1; j <= N; j++) {
                int k = a * i * i + b * j * j;
                if(k >= 0)
                    sum1[k]++;
                else
                    sum2[-k]++;
            }
        for(int i = 1; i <= N; i++)
            for(int j = 1; j <= N; j++) {
                int k = c * i * i + d * j * j;
                if(k > 0)
                    sum += sum2[k];
                else
                    sum += sum1[-k];
            }
 
        // 每个解有正有负,所以结果有2^4种
        printf("%d\n", 16 * sum);
    }
 
    return 0;
}

 

posted @ 2019-08-16 20:12  Shmilky  阅读(145)  评论(0编辑  收藏  举报