Hash Algorithm 初步 & BZOJ2081 POI2010 Beads


POI2010 题解整理

题目大意

给定一串珊瑚珠子(长度 Len2105 ),每个珠子属于种类k。现将该串珠子按照len的长度从开头依次断开,求得到最多不相同的子项链个数,并输出所有满足这个最大值的len长度。

如果分到最后,这条珠子的剩下长度不满足len,忽略最后的部分。项链是可以反转的,即(1,2,3)和(3,2,1)表示同一种样式。

Input

  • 第一行一个整数n( n2105 )代表珠子的长度。
  • 第二行是由空格分开的颜色 ai (1<= ai <=n)。

Output

  • 第一行两个整数,第一个整数代表能获得的最大不同的子串长度,第二个整数代表能获得最大值的k的个数,第二行输出所有的k,用空格隔开。

Sample Input

21
1 1 1 2 2 2 3 3 3 1 2 3 3 1 2 2 1 3 3 2 1

Sample Output

6 1
2


这个Hash初步本来是要放在 USACO 2015 February Bronze&Silver Censoring 这一道题目上的,但是考虑 坑爹的 版权问题所以只好放到这道题上了(反正都是水题)。


Hash算法:

  • Hash的本质是将原来的字符串转化成用B进制表示的数。对于一段字符串,我们通过进制转化,将其变成一个B进制的数来进行维护,是远比直接字符串维护要快的。

预处理操作

同十进制一样,我们需要预处理出每一位上对应的B进制幂的值

举个栗子,如果我们知道数字9和“在千位上添加9”的操作,我们只需要+9*Base【千位(=1000)】=9000即可完成。而将十进制改成B进制就同理了。得到B进制幂数组Base[i]。

显然这个Base[i]是会int溢出的,而一般情况下,我们使用的是Base[i]对素数取模过后的数值,这个数值在对较小数取模情况下容易产生冲突,但是用较大素数(最常用的是孪生素数1e9+7和1e9+9)冲突几率就会很小。

实时计算操作

如果我们不是直接按照对应位更新,那么字符串后面添加一个字符的操作等价于将前面的数值全部向前移动一位(有二进制<<操作符的意味),再将最后一位添加上这个数值,计算公式如下:

Hi=(Hi1B+(char)c)%P

判重操作

一般的情况是让我们判断目前已Hash字符串中中间某一段是否与当前询问的字符串等价。两者的区别是:

  • 中间Hash的某一段相对于当前询问字符串,被移动了几个数位。

如果要成功判重,就需要将中间这段Hash重新移回到初始位置(这个操作似二进制>>操作符)。

举个栗子,如果我们要提取出 (1234567)B 中的——

  • (1234)B ,按照之前的计算操作,我们可以知道H[4]就是 (1234)B 的哈希值,所以我们可以直接拿来用。
  • (567)B ,已经知道H[4]等价于 (1234)B ,H[7]等价于 (1234567)B ,那么 (567)B 哈希值由下面这个公式得到:
    w[i,j]=(HjHi1Base[ji+1])%P
    此处Base的下标需要特别注意,显然 [1,i] 的这段位移长度是当前需要Hash的字符串开头到最后的长度。

Hash示例:

#include <cstdio>
#define B 233 //B 表示B进制
#define P 1000000009
//1e9+7,1e9+9是孪生素数,用这两个数可以卡除hash值重复的情况

char S[M];
int Base[M],H[M];

int Hash(int L,int R){
    int num=H[R]-(1LL*H[L-1]*Base[R-L+1])%P;
    if(num<0)num+=P;//可能得到的num为负值
    return num;
}//123456,取出456,有123456-123*10^3=456

int main(){
    scanf("%s",S+1); n=strlen(S+1);
    Base[0]=1;H[0]=0;
    for(int i=1;i<=n;i++)Base[i]=(1LL*Base[i-1]*B)%P;
    for(int i=1;i<=n;i++)H[i]=(1LL*H[i-1]*B+S[i])%P;
    int num=Hash(L,R);
    return 0;
}

其实这题暴力模拟过来就好了,我们只需要通过下面这个公式来证明一下暴力操作的复杂度真的不高:

δ(n)=n1+n2++nnnlogn
这仅仅就是著名的调和级数变形罢了:
11+12++1n=ln(n+1)+rEuler
也就是说,我们将其暴力全部拆开的时间也只需要与 O(nlogn) 等阶的复杂度。

为了不拖累这么好的复杂度,我们需要在较小的复杂度内完成对当前字符串和前面所有字符串进行判重的操作。想到判重我们一般采用set O(logn) 判重。但是只要是直接对字符串进行判重而不是一个数值就一定会带上 O(n) 的复杂度。这也是Hash算法最基本的目的所在。

利用上述的Hash算法,我们就可以在 O(nlog2n) 的复杂度内完成这道题。

但实际上这题由于poi完美的多组数据,简单的Hash是无法避免冲突的。于是那么我们就需要多种降低冲突几率的方法:

  • 将Hash数值的类型更改为double或者(unsigned) long long。这样需要判断的数值范围会相应提高。
  • 采用孪生素数一起判重的方法,此时set内的元素就是 pair(Hash1,Hash2) 了。这个方法是最保险的,极大地降低的冲突可能。
  • 剩下的就是积累一些判重能力比较厉害的素数常数,譬如说本题采用B=200019居然神奇地卡了过去。
include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <set>
#define M 200005
#define B 1000037
#define P 1000000009
#define T 1000000007
#define Pair pair<int,int> 
using namespace std;
int n,A[M];
int Base1[M],Hnxt1[M],Hback1[M];
int Base2[M],Hnxt2[M],Hback2[M];
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&A[i]);

    Base1[0]=1;for(int i=1;i<=n;i++)Base1[i]=1LL*Base1[i-1]*B%P;
    Hnxt1[0]=0;for(int i=1;i<=n;i++)Hnxt1[i]=(1LL*Hnxt1[i-1]*B+A[i])%P;
    Hback1[n+1]=0;for(int i=n;i>=1;i--)Hback1[i]=(1LL*Hback1[i+1]*B+A[i])%P;

    Base2[0]=1;for(int i=1;i<=n;i++)Base2[i]=1LL*Base2[i-1]*B%T;
    Hnxt2[0]=0;for(int i=1;i<=n;i++)Hnxt2[i]=(1LL*Hnxt2[i-1]*B+A[i])%T;
    Hback2[n+1]=0;for(int i=n;i>=1;i--)Hback2[i]=(1LL*Hback2[i+1]*B+A[i])%T;

//  for(int i=1;i<=n;i++)printf("%d\n",(Hnxt[i]-1LL*Hnxt[i-1]*B%P+P)%P);//
//  for(int i=n;i>=1;i--)printf("%d\n",(Hback[i]-1LL*Hback[i+1]*B%P+P)%P);

    int ans=0;vector<int>Ans;
    for(int step=1;step<=n;step++){
        if(n/step<ans)break;
        set<Pair>S;int cnt=0;
        int L=1,R=L+step-1;
        while(R<=n){
            int Hashnxt1=(Hnxt1[R]-1LL*Hnxt1[L-1]*Base1[R-L+1]%P+P)%P;
            int Hashback1=(Hback1[L]-1LL*Hback1[R+1]*Base1[R-L+1]%P+P)%P;

            int Hashnxt2=(Hnxt2[R]-1LL*Hnxt2[L-1]*Base2[R-L+1]%T+T)%T;
            int Hashback2=(Hback2[L]-1LL*Hback2[R+1]*Base2[R-L+1]%T+T)%T;

            Pair Hashnxt=make_pair(Hashnxt1,Hashnxt2);
            Pair Hashback=make_pair(Hashback1,Hashback2);

            if(S.find(Hashnxt)==S.end()&&S.find(Hashback)==S.end()){
                cnt++;
                S.insert(Hashnxt),S.insert(Hashback);
            }
            L+=step,R+=step;
        }
        if(cnt>ans){ans=cnt;Ans.clear();}
        if(cnt==ans)Ans.push_back(step);
    }
    printf("%d %d\n",ans,Ans.size());
    for(int i=0;i<Ans.size();i++)
        printf("%d%c",Ans[i],(i==Ans.size()-1)?'\n':' ');
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值