2019 ICPC上海网络赛 G. Substring 哈希+尺取法+unordered_map

时间:2019-09-17
本文章向大家介绍2019 ICPC上海网络赛 G. Substring 哈希+尺取法+unordered_map,主要包括2019 ICPC上海网络赛 G. Substring 哈希+尺取法+unordered_map使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

题目链接:https://nanti.jisuanke.com/t/41415

赛后补题。

参考博客:https://blog.csdn.net/bjfu170203101/article/details/100889468

题意:给出一个主串(假设长度为m),再给出n个模式串,对于每一个模式串,如果在主串中有一个子串,它的第一个字符和最后一个字符分别与这个模式串的开头字符和结尾字符相同,并且每一种字符出现的次数相同,那么就把这个模式串和这个子串看成是相同的,认为模式串在主串中出现过一次,比如模式串是:abccd,那么主串中如果有子串accbd或者acbcd这些,都可以认为模式串和子串是相同的,但是对于dbcca,accd这些,则是认为不相同的。

现在我们需要计算出每一个模式串在主串中出现的次数。

思路:对每一个模式串求它的哈希值,把所有子串中出现的哈希值都放在unordered_map中,我们可以用尺取法在O(n)的时间复杂度中遍历一次模式串,找所有长度都为len的模式串在主串中出现的次数,有多少种不同的长度就遍历主串多少次,这样看起来貌似时间复杂度最坏是O(n*m),但是这道题有一个条件就是所有模式串的长度之和不会超过1e5,那么就说明最多只会有大约sqrt(2e5)个不同的长度,因为1+2+......+n=n*(1+n)/2=1e5。

这样实际上我们最多只需要遍历sqrt(2e5)次主串就行了,时间复杂度是O(sqrt(2e5)n)。

这里面我们还需要找一个构造哈希值的方法,使得所有首尾字符相同,并且中间各中字符出现次数相同的串的哈希值一定相同,并且还需要尽量降低哈希冲突的概率(降低什么的我也不太懂)。

因为除去首尾字符中间出现的字符只关心出现次数,不关心出现次序,所以选择用加法,对于每个出现的字符,我都加上它的Pow(base,s[i]-'a'+1),我这里base是131,假如s[i]='a',那么就是加上131^1,s[i]='b'就加上131^2,以此类推。这个base一般是一个质数,这样我们就得到了模式串除去首尾字符的哈希值(假设是value),然后我把value=value*10000,然后再

value+=s[0]-'a'+1,value+=(s[len-1]-'a'+1)*100,实际上就是将模式串的首字符经过处理后的值放在个位和十位,将尾部字符经过处理后的值放在百位和千位,这样我们就可以保证首尾字符相同,并且中间各种字符出现次数相同的字符串的哈希值一定是相同的。

对于子串abac,去掉首尾字符就是ba,value=base^2+base^1=17292。value=value*10000=172920000,把首尾加进去就是172920301,

03是尾部字符,01是首部字符。

同时我们在求主串中某个长度的所有子串的哈希值的时候不需要每次都重新求他们的哈希值,可以用尺取法根据前后之间的联系在O(1)的时间中算出向右移动一个单位的哈希值。

我们使用unordered_map,好像比使用map少一个logn的时间复杂度(貌似,好像,也许,可能)。

代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
#include<map>
#include<tr1/unordered_map>
#include<stack>
#include<cmath>
#include<vector>
#include<set>
#include<cstdio>
#include<string>
#include<deque> 
using namespace std;
using namespace std::tr1;
typedef unsigned long long ll;
#define eps 1e-8
#define INF 0x3f3f3f3f
#define maxn 100005
const int base=131;//一个质数 
const ll mod=100280245065;//一个12位的质数
ll f[30];
char s[maxn],ss[maxn];
unordered_map<ll,int>mp;
ll value[maxn];
int len[maxn];
int n,m,k,t,cnt;
ll Pow(ll a,ll b){
    ll ans=1;
    while(b){
        if(b&1)
        ans=(ans*a)%mod;
        a=(a*a)%mod;
        b>>=1;
    }
    return ans;
}
ll getHash(char *s,int l,int r){//求字符串s在区间[l,r]中的子串的哈希值 
    ll ans=0;
    for(int i=l+1;i<=r-1;i++){//求去掉首尾字符的值 
        ans=(ans+f[s[i]-'a'+1])%mod;
    }
    ans*=10000;//将ans往前挪4个十进制位置 
    ans+=s[l]-'a'+1;//子串中首字符的值放在个位和十位中 
    ans+=(s[r]-'a'+1)*100;//子串中的尾部字符的值放在百位和千位中 
    return ans;
}
int main()
{
    scanf("%d",&t);
    f[0]=1;
    for(int i=1;i<30;i++)
    f[i]=(f[i-1]*base)%mod;//f[i]就是(base^i)%mod 
    while(t--){
        mp.clear();
        scanf("%s",s);
        m=strlen(s);//m是主串的长度 
        scanf("%d",&n);
        cnt=0;
        for(int i=0;i<n;i++){
            scanf("%s",ss);
            len[i]=strlen(ss);
            value[i]=getHash(ss,0,len[i]-1);//得到当前字符串的哈希值 
            mp[value[i]]=1;//当前字符串出现过 
        }
        sort(len,len+n);
        int size=unique(len,len+n)-len;//去重,得到不同长度的个数 
        for(int i=0;i<size;i++){
            ll now; 
            for(int j=0;j+len[i]-1<m;j++){
                if(j==0) 
                now=getHash(s,j,j+len[i]-1);
                else{//因为是尺取法,所以可以不用重新求哈希值,只需要左右边界处理一下就行了 
                    now/=10000;//去掉前一个子串两边的字符的值 
                    if(len[i]>2)//判断一下长度是否大于2 
                    now=(now-f[s[j]-'a'+1]+f[s[j+len[i]-2]-'a'+1]+2*mod)%mod;//从区间[j-1,j+len[i]-2]的除去首尾字符now推出区间[j,j+len[i]-1]除去首尾字符的now 
                    now*=10000;
                    now+=s[j]-'a'+1;//重新加上首尾字符处理后的值 
                    now+=(s[j+len[i]-1]-'a'+1)*100;
                }
                if(mp.find(now)!=mp.end()){//判断主串中是否有这个模式串 
                    mp[now]++; 
                }
            }
        }
        for(int i=0;i<n;i++)
        printf("%d\n",mp[value[i]]-1);
    }
    return 0;
}

原文地址:https://www.cnblogs.com/6262369sss/p/11537356.html