Hash学习笔记

@

Hash简介:

对于某些题目而言,在判断相等的时候,有时我们来不及或不会打一些算法了,甚至有时候连状压都会炸,这个时候,Hash无非是最好的选择。

Hash能够将一些状态通过一些方式把他们压缩到一个区间里面的某些值,并用bool类型来判重就可以了,耶!

题面懒得写了,传送门

一道不错的模版。KMP乱入

这道题是字符串Hash。

我们定义两个互质的常量,\(b\)\(h\)。(注意\(b<h\))

对于一个字符串\(a\),我们定义他的Hash函数值为\(C(a,n)\),代表\(a\)字符串从\(1\)\(n\)的字符子串的Hash值,函数的解析式是什么呢?

我们就定义为:
妈妈,我终于会写数学公式了

\[C(a,n)=(a[1]*b^{n-1}+a[2]*b^{n-2}+...+a[n]*b^{0})\bmod h \]

现在,我们设A字符串长度为\(n\),那我们就是求\(C(a,i(i\in[1,n]))\)
怎么求?
暴力每次把1到i的子串算一遍
肯定有更简便的做法。

\[C(a,i)=C(a,i-1)*b+A[i] \]

但是问题怎么求?我们设\(m\)为B串的长度,\(ed=C(B,m)\)

那么,我们就要求\(a\)串中任意一个长度为\(m\)的子串的Hash值来做比较,首先,设这个子串的最左边的字符在\(a\)的位置为\(l\),而最右边的为\(r\),且\(r-l+1=m\),那么,我们现在已经知道了\(C(a,r)\)\(C(a,l-1)\)

那么怎么搞?

那么我们是不是要将\(C(a,r)\)\(C(a,l-1)\)\([1,l-1]\)的部分抵消掉。

\[C(a,r)-C(a,l-1)? \]

不不不。
仔细观察,我们发现,在\(C(a,r)\)\(C(a,l-1)\)的值被放大了\(b^{m}\)
因为\(r-(l-1)=r-l+1=m\)

那么,式子出来了。

\[C(a,r)-C(a,l-1)*b^{m}=C(a,r)-C(a,r-m)*b^{m}\pmod h \]

那么,这样只要枚举r,然后就可以计算出在A中\(r-n+1\)\(r\)的子串的Hash值了。

不过得先算出\(b^{m}\)。。。

当Hash值相同时,即为相等。

Hash为什么不会冲突?
我怎么知道,好像因为生日悖论,总之冲突的几率挺小的,当然\(h\)尽量定大一点的一个素数,如1e8+7与1e8+9,而\(b\)的取值随便啦。

不过,Hash冲突的几率还是让我们放不下心来(不过在比赛中一般可以认为不冲突),有时为了保险起见,许多都用双哈希或三哈希,即将\(h\)值换一换,一般双哈希取1e8+7与1e8+9,概率就已经很小的了。

当然\(h\)如果取\(2^{32}\)\(2^{64}\),即为unsigned int或unsigned long long自然溢出,省去mod的时间也可以。

本代码只用单哈希。

#include<cstdio>
#include<cstring>
using  namespace  std;
typedef  long  long  ll;
const  ll  h=100000007,b=130;//h的取值与b的取值
ll  pp[1100000]/*求b的i0次方*/,tp[1100000]/*Hash值*/,n,m,ed,ans;
char  st[1100000],stp[1100000];//A串与B串
int  main()
{
    scanf("%s%s",st+1,stp+1);n=strlen(st+1);m=strlen(stp+1);//长度
    pp[1]=b;
    for(int  i=2;i<=n;i++)pp[i]=(pp[i-1]*b)%h;//计算
    for(int  i=1;i<=n;i++)tp[i]=(tp[i-1]*b+st[i])%h;//求Hash值
    for(int  i=1;i<=m;i++)ed=(ed*b+stp[i])%h;//求B串的Hash值
    for(int  i=m;i<=n;i++)
    {
        ll  x;x=((tp[i]-tp[i-m]*pp[m])%h+h)%h;//求子串Hash值
        ans+=(x==ed);//统计答案
    }
    printf("%lld\n",ans);
    return  0;
}

Hash表

我承认我懒

我们有时候需要表示一个集合{\(1,2,11,22,1000\)}
并且以后还要时不时的加入新的数,并且删除原有的数,还有查找一些数字是否存在。

离散化
你家离散化支持强制在线?

我们用一个长度为5的数组存,但是时间巨大,不得不让我们面壁思过考虑新的做法。
我们用Hash?
bool?你自己玩去吧,就算\(O(1)\)时间,空间定这么大,亏呀!
某大佬:Hash表是什么?直接Hash硬上就行了

好吧,Hash确实可以做,但是要你算出这个数字的位置,你就得定int了。。。
在这里插入图片描述

这个时候,我们不得不借力新的数据结构,Hash表!

What is this?

如同Hash一样,我们定义一个函数: \(C(x)=x\bmod k\)\(k\)你自己定。
我们让\(k=11\)
那么我们定一个11的数组就好了,耶!

这很像Hash,但不是Hash。
如果是Hash就重复了,\(11,22\)

你自己撞枪口上还硬说Hash是错的

人家有自己的名字,Hash函数。不就是Hash

Hash函数适用于Hash表中。

继续。

那么,我们可以把11与22一起丢到0里面去,什么意思?

我们将每个位置定义一个单向链表,而\(last[i]\)记录的是最后一个丢到这个位置的节点的编号。

如图:
在这里插入图片描述

好丑。。。

图就是这样,一个单向链表,不过左边的那一个\(0,1,2,3,4,...,10\)不用定义,直接用\(last[i]\)代表第\(i\)个位置插入的最后一个数的编号是多少就行了。

找的话先计算一遍要找的数的函数值,然后找到位置跳链表就行了,速度也挺快的。

但是Hash表的速度取决与Hash函数的值分布的均不均匀。

比较均匀的Hash函数从书上学的分几类:

  1. 除余法。
    就是模以一个数字\(k\)而已。但是如果运气不好定义的\(k\)是大部分数的公因数。。。那你就是非酋了。。。所以\(k\)最好定义一个大一点的质数,如:1000003、1000019、999979、100000007、100000009。

  2. 乘积取整法
    乘一个在\((0,1)\)范围内的实数A,最好是无理数,\(\frac{\sqrt{5}-1}{2}\)是一个实际效果很好。貌似这句话有点眼熟。。。 然后再取小数,乘以Hash表的大小向下取整。
    不过这样的话,若数字集中一块,估计很快就凉了,而且各种操作还会导致速度变慢。

  3. 基数转换法
    将原来的数字当成\(k(k>10)\)进制对待,然后转成10进制,就是转换过程稍稍慢了点。

  4. 平方取中法
    将x取平方,然后根据Hash表实际大小能存多少位,来取中间几位,就是转换过程慢了点,但是分布就挺均匀的。

当然,只要速度快,都是好的Hash函数,Hash函数总的来说还是得根据题目定,不过因为是随机数据,我一般用除余法,方便,还挺快!

对于上面那道题,我们可以假设每个字符串的值就是他的Hash函数,\(b=300,h=100000007\),然后再用除余法走一遍,貌似没什么问题的样子。

#include<cstdio>
#include<cstring>
#include<algorithm>
using  namespace  std;
typedef  long  long  ll;
const  ll  b=300,h=100000007,k=1000019;//常数
struct  node
{
    ll  l/*前一个插入到这个位置的数*/,y/*原本的数字*/;//单向链表
}a[310000];ll  last[1100000]/*Hash表中的last数组*/,n,len/*整个hash表有多少节点*/;
char  sp[30000];//char数组
inline  ll  Hash()//求每个字符串的hash值
{
    ll  ans=0,tlen=strlen(sp+1);
    for(ll  i=1;i<=tlen;i++)ans=(ans*b+sp[i])%h;
    return  ans;
}
inline  void  add(ll  x)//添加x
{
	ll  y=x;x%=k;//除余法
    for(ll  i=last[x];i;i=a[i].l)
    {
        if(a[i].y==y)return  ;//如果原本有的,就不加了。
    }
    a[++len].y=y;a[len].l=last[x];last[x]=len;//添加。
}
inline  bool  find(ll  x)//寻找
{
	ll  y=x;x%=k;//除余法
    for(ll  i=last[x];i;i=a[i].l)
    {
        if(a[i].y==y)return  true;//找到了,返回true
    }
    return  false;
}
char  st[2][10]={"no","yes"};
int  main()
{
    scanf("%lld",&n);
    for(ll  i=1;i<=n;i++)
    {
        scanf("%s",sp+1);
        if(sp[1]=='a')
        {
            gets(sp);//注意,用get的话会把add与字符串中间的空格读进来,所以不用+1
            add(Hash());//添加
        }
        else
        {
            gets(sp);//同理
            printf("%s\n",st[ find(Hash()) ]);//寻找然后输出
        }
    }
    return  0;
}

更多的题目来了。。。


First

暴力枚举子字符串长度,时间\(O(nlogn)\)我也不知道为什么

#include<cstdio>
#include<cstring>
#include<cstdlib>
using  namespace  std;
typedef  long  long  ll;
const  ll  b=300,h=100000007;
ll  hash[2100000],n,po[2100000];
char  st[2100000];
inline  ll  inhash(ll  x,ll  y){return  ((hash[y]-hash[x]*po[y-x])%h+h)%h;}//计算x+1到y的hash值
int  main()
{
    po[1]=b;
    for(ll  i=2;i<=1000000;i++)po[i]=(po[i-1]*b)%h;//计算阶乘
    while(1)
    {
        scanf("%s",st+1);
        n=strlen(st+1);
        if(n==1  &&  st[1]=='.')break;
        for(ll  i=1;i<=n;i++)hash[i]=(hash[i-1]*b+st[i])%h;//计算hash值
        for(ll  i=1;i<=n;i++)
        {
            if(n%i==0)//一个优化
            {
                bool  bk=true;
                for(ll  j=i;j<=n;j+=i)
                {
                    if(inhash(j-i,j)!=hash[i])//判断
                    {
                        bk=false;
                        break;
                    }
                }
                if(bk==true)//可以,输出
                {
                    printf("%lld\n",n/i);
                    break;
                }
            }
        }
    }
    return  0;
}

Second

暴力枚举前缀长度,每次\(O(n)\)。。。

#include<cstdio>
#include<cstring>
using  namespace  std;
typedef  long  long  ll;
const  ll  b=300,h=100000009;
ll  hash[510000],po[510000],n,ans[510000];
char  st[510000];
inline  ll  inhash(ll  x,ll  y){return  ((hash[y]-hash[x]*po[y-x])%h+h)%h;}//求[x+1,y]的hash值。
int  main()
{
    po[1]=b;
    for(ll  i=2;i<=400000;i++)po[i]=(po[i-1]*b)%h;//预处理阶乘
    while(scanf("%s",st+1)!=EOF)
    {
        ans[0]=0;
        n=strlen(st+1);
        for(ll  i=1;i<=n;i++)hash[i]=(hash[i-1]*b+st[i])%h;//hash值
        for(ll  i=1;i<=n;i++)
        {
            if(inhash(0,i)==inhash(n-i,n))ans[++ans[0]]=i;//记录
        }
        for(ll  i=1;i<ans[0];i++)printf("%lld ",ans[i]);//输出
        printf("%lld\n",ans[ans[0]]);
    }
    return  0;
}

Third

没错,枚举那个位置是插进去的,然后推一下如何计算剔除这个位置的hash值,然后就没毛病了。

#include<cstdio>
#include<cstring>
#include<cstdlib>
using  namespace  std;
typedef  long  long  ll;
const  ll  b=300,h=100000009;
ll  po[2100000],hash[2100000],n,m,kk/*kk就是个优化*/,ans,cnt=0,sit=0;/*统计答案三人组*/
char  st[2100000];
inline  ll  inhash(ll  x,ll  y){return  ((hash[y]-hash[x]*po[y-x])%h+h)%h;}//求出[x+1,y]的hash值。
inline  ll  hash_hash(ll  x,ll  y,ll  k)//在[x,y]的区间里把k剔除
{
    ll  xk=inhash(x-1,k-1),ky=inhash(k,y);//计算出两段hash值
    return  (xk*po[y-k]+ky)%h;//拼在一起
}
int  main()
{
    scanf("%lld",&n);
    scanf("%s",st+1);
    po[0]=1;/*因为在inhash中会出现po[0]的情况*/po[1]=b;
    for(ll  i=2;i<=n;i++)po[i]=(po[i-1]*b)%h;
    for(ll  i=1;i<=n;i++)hash[i]=(hash[i-1]*b+st[i])%h;
    m=n/2+1;
    kk=inhash(m,n);
    for(ll  i=1;i<=m;i++)//模拟字符在前一个字符串里
    {
        if(hash_hash(1,m,i)==kk)
		{
            if(ans==1)//判断
            {
                if(kk!=sit)//不是同一个字符串
                {
                    printf("NOT UNIQUE\n");//不止一组解
                    return  0;
                }
            }
			else  ans++,cnt=i,sit=kk;//统计答案
		}
        if(ans==2)
        {
            printf("NOT UNIQUE\n");//不止一组解
            return  0;
        }
    }
    kk=inhash(0,m-1);//减少hash的步骤
    for(ll  i=m;i<=n;i++)//模拟字符在后一个字符串里
    {
        if(hash_hash(m,n,i)==kk)
        {
            if(ans==1)//判断
            {
                if(kk!=sit)//不是同一个字符串
                {
                    printf("NOT UNIQUE\n");//不止一组解
                    return  0;
                }
            }
			else  ans++,cnt=i,sit=kk;//统计答案
        }
    }
    if(ans==0)printf("NOT POSSIBLE\n");//输出
    else//输出字符串
    {
        if(cnt<=m)
        {
            for(ll  i=1;i<=m;i++)
            {
                if(i!=cnt)printf("%c",st[i]);
            }
        }
        else
        {
            for(ll  i=m;i<=n;i++)
            {
                if(i!=cnt)printf("%c",st[i]);
            }
        }
        printf("\n");
    }
    return  0;
}

其实我也发现我代码有很多多余的判断,反正都A了,不管他


可恶至极的一道题!

像上面一样?\(O(qnlogn)\),显然,毒瘤出题人不会让我们过。。。

我们考虑优化。

以前我们判断这个是否是循环节,我们都是for一遍,但是,有更好的方法。

假设当前区间为\([l,r]\),循环节长度为\(k(k|(r-l+1))\),那么,当\([l+k,r]\)的Hash值等于\([l,r-k]\)的Hash值时,就代表这个是循环节!

为什么?

假设长度为\(k\)的循环节在长度为\(n\)的字符串中标号为\(a_{1},a_{2},a_{3},...,a_{n/k}(a_{1},a_{2},a_{3}...\)的长度都为\(k)\)
那么,Hash值相等代表\(a_{1}=a_{2}...\),所以可以呀。

其实想想都觉的可以。。。

但是时间复杂度还是\(O(qn)\),如果可以过,就不会有出题人收到刀片的事情了。。。

由于我们判断了\(k\)是否是\(n\)的约束了,所以时间为\(O(q\sqrt{n})\)

但是还是不能过!

考虑循环节的性质,如果\(k\)是其中一个循环节,那么\(ki(ki|n)\)(ki代表\(k*i\),i为任意数字)也是一个循环节,所以我们先线性筛一遍,记录出每个数的最小约数,然后初始化\(now=n\)代表当前的循环节最小为\(now\),不断找\(n\)的约数\(k_{i}\)判断\(now\)能否继续分成\(k_{i}\)块,可以的话\(now/k_{i}\),代表当前的循环节还可以分下去,知道找完所有约数为止。

那么如何找到所有的约数?
由于我们记录了每个数的最小约数(记录这个会比较好写),那么初始化\(v=n\),然后不断取\(v\)的最小约数,然后再除于这个最小的约数,不断下去,就行了。

丑陋的卡常代码,为了卡常,我用了溢出的方法来进行模。

#include<cstdio>
#include<cstring>
#define  RI  register  int//寄存器
using  namespace  std;
typedef  unsigned  long  long  ll;
const  ll  b=53;//一个非偶数质数
ll  ans,n,m;
ll  hash[510000],po[510000];//日常数组
char  ss[510000];
inline  void  in(ll  &x)//快读
{
    x=0;char  c=getchar();
    while(c<'0'  ||  c>'9')c=getchar();
    while('0'<=c  &&  c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
}
inline  void   out(ll  x)//快输
{
    register  ll  y=10;
    while(y<=x)y=(y<<3)+(y<<1);
    while(y!=1)y/=10,putchar(x/y+48),x%=y;
    putchar('\n');
}
inline  ll  inhash(ll  x,ll  y){return  hash[y]-hash[x]*po[y-x];}
inline  bool  check(ll  x,ll  y,ll  t)//在x到y区间,循环节长度为t
{
    register  ll  xt=inhash(x-1,y-t),yt=inhash(x+t-1,y);//卡常大法好
    if(xt==yt)return  true;
    return  false;
}
bool  f[510000];
int  pri[510000],next[510000];
inline  void  shuai()//欧拉筛
{
    for(int  i=2;i<=n;i++)
    {
        if(f[i]==false)
        {
            pri[++pri[0]]=i;
            next[i]=i;//素数就时他自己
        }
        for(int  j=1;j<=pri[0]  &&  i*pri[j]<=n;j++)
        {
            f[i*pri[j]]=true;
            next[i*pri[j]]=pri[j];//自己想想为什么是最小约数
            if(i%pri[j]==0)break;
        }
    }
}
int  main()
{
    in(n);
    scanf("%s",ss+1);
    po[0]=1;po[1]=b;
    for(RI  i=2;i<=n;i++)po[i]=po[i-1]*b;
    for(RI  i=1;i<=n;i++)hash[i]=hash[i-1]*b+ss[i];
    shuai();//初始化
    in(m);
    for(RI  i=1;i<=m;i++)
    {
        ll  l,r;in(l),in(r);
        ll  v=ans=r-l+1;//如文中所说,ans就是now
        while(v!=1)
        {
            if(check(l,r,ans/next[v]))ans/=next[v];
            v/=next[v];
        }
        printf("%llu\n",ans);//输出
    }
    return  0;
}

让我想报复社会报答出题人的一道题。。。

首先,记住两个数字,2333与98754321,膜了题解才AC,变态出题人!

把数组当成字符串,暴力枚举呀,用map判重突然发现这玩意挺牛逼的!\(O(n\log^{2}{n})\),就是前一遍Hash,后一遍Hash。

其实虽然\(b,h\)参数并没变,但是两次Hash的方法不同也可以起到很厉害的作用。

优化:如果当前枚举的块的长度所分的块还没有\(anss\)(目前能获得的最大不同的段的个数)大,那么退役吧。。。 break

#include<cstdio>
#include<cstring>
#include<map>//map大法好呀!
#define N 200010//数组大小
using  namespace  std;
typedef  long  long  ll;
const  ll  b=2333,h=98754321;//变态出题人
map<ll,bool> ss;//map
ll  n;
ll  hash1[N]/*正着*/,hash2[N]/*倒着*/,po[N],ans[N],anss/*记录能分成多少段*/,a[N];
inline  ll  inhash1(ll  x,ll  y){return  ((hash1[y]-hash1[x]*po[y-x])%h+h)%h;}//正着[x+1,y]
inline  ll  inhash2(ll  x,ll  y){return  ((hash2[x]-hash2[y]*po[y-x])%h+h)%h;}//倒着[x,y-1]
int  main()
{
    scanf("%lld",&n);
    po[0]=1;
    for(ll  i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
        po[i]=(po[i-1]*b)%h;//次方
        hash1[i]=(hash1[i-1]*b+a[i])%h;//正着
    }
    for(ll  i=n;i>=1;i--)hash2[i]=(hash2[i+1]*b+a[i])%h;//倒着
    for(ll  i=1;i<=n;i++)
    {
        if(n/i<anss)break;//优化
        ss.clear();//清空
        ll  now=0;//目前所能获得的最大块数
        for(ll  j=i;j<=n;j+=i)
        {
            ll  zheng,dao;
            zheng=inhash1(j-i,j);dao=inhash2(j-i+1,j+1);
            if(!ss.count(zheng)  ||  !ss.count(dao))//不用质疑,打&&会WA,可能是因为用Hash其中有一个会重复,这也体现了正着来一遍,反着来一遍可以起到判重作用
            {
                ss[zheng]=true;ss[dao]=true;//扔到map里面去
                now++;
            }
        }
        if(now==anss)ans[++ans[0]]=i;//新增答案
        else  if(now>anss)//更新新的答案
        {
            anss=now;
            ans[ans[0]=1]=i;
        }
    }
    printf("%lld %lld\n",anss,ans[0]);
    for(ll  i=1;i<ans[0];i++)printf("%lld ",ans[i]);
    printf("%lld\n",ans[ans[0]]);//输出
    return  0;
}

终于有二分的题了

二分加Hash

首先串必须是偶数长度(不然你中间的字符去反后就不等于自己了)

而且,若\([l,r]\)是反对称,那么\([l-1,r-1]\)也是反对称。

正着一遍Hash,弄成反串再反着来一遍Hash,那么,假设我们有两个数字\(l,k\),那么,我们就是求\([l,l+2*k-1]\)

那么,先用循环\(i\)枚举\(l+k-1\),然后,然后里面用二分枚举\(l\),然后用\(i\)\(l\)计算出\(r\),设二分结果为\(x\),然后\(ans+=(i-x)+1\)

代码:

#include<cstdio>
#include<cstring>
#define  N  1100000
using  namespace  std;
typedef  long  long  ll;
const  ll  b=2333,h=100000007;
ll  hash1[N]/*正着*/,hash2[N]/*反串反着哈希*/,po[N]/*次方*/,n,ans;
char  st[N];
inline  ll  inhash1(ll  x,ll  y){return  ((hash1[y]-hash1[x]*po[y-x])%h+h)%h;}//[x+1,y]
inline  ll  inhash2(ll  x,ll  y){return  ((hash2[x]-hash2[y]*po[y-x])%h+h)%h;}//[x,y-1]
inline  ll  mymax(ll  x,ll  y){return  x>y?x:y;}//取最大 
inline  bool  check(ll  l,ll  r)
{
    ll  zh=inhash1(l-1,r)/*正着*/,da=inhash2(l,r+1)/*倒着*/;
    return  zh==da;
}
int  main()
{
    scanf("%lld",&n);
    scanf("%s",st+1);
    po[0]=1;
    for(int  i=1;i<=n;i++)
    {
        po[i]=(po[i-1]*b)%h;
        hash1[i]=(hash1[i-1]*b+st[i])%h;
    }
    for(int  i=n;i>=1;i--)hash2[i]=(hash2[i+1]*b+((st[i]-48)^1)+48)%h;//计算hash 
    for(int  i=1;i<=n;i++)
    {
        ll  l=mymax(1,i-(n-i)+1)/*计算l区间*/,r=i,mid,x=i+1/*记录答案*/;//二分 
        while(l<=r)
        {
            mid=(l+r)/2;
            if(check(mid,i+(i-mid+1)))r=mid-1,x=mid;//成立,缩小限制 
            else  l=mid+1;//不成立,放大限制 
        }
        ans+=(i-x)+1;//统计 
    }
    printf("%lld\n",ans);//输出 
    return  0;
}

第一道Hash表的练习题

终于来了道Hash表的题,但是好水。QAQ

#include<cstdio>
#include<cstring>
using  namespace  std;
typedef  long  long  ll;
const  int  k=2999999;//一个好看的质数。。。
struct  node
{
    int  l;
    ll  x;
}hash[2100000];int  len,last[k];
inline  void  add(ll  x)//x肯定没出现过
{
    ll  y=x;x%=k;//转换
    hash[++len].x=y;hash[len].l=last[x];last[x]=len;
}
inline  bool  find(ll  x)//找到x
{
    ll  y=x;x%=k;
    for(int  i=last[x];i;i=hash[i].l)//找Hash表
    {
        if(hash[i].x==y)return  1;
    }
    return  0;
}
int  main()
{
    ll  A,B,C,x=1,id=0;scanf("%lld%lld%lld",&A,&B,&C);A%=C;//不加也可以QAQ
    while(!find(x))
    {
        if(id==2000000){id=-1;break;}//-1判断
        add(x);//添加
        x=(A*x+(x%B))%C;id++;//更新
    }
    printf("%lld\n",id);
    return  0;
}

一遍AC的快感

这道题的话,一个支持删除的Hash表,也不是很难码。

先说做法,维护\(l,r\)两个下标指针,每次用循环让\(r++\),然后找一下有没有与第\(r\)个位置重复的,有,就让\(l\)等于他,同时删掉之前的数字,然后用\(ans\)记录最大的区间。

#include<cstdio>
#include<cstring>
#include<cstdlib>
using  namespace  std;
typedef  long  long  ll;
const  ll  k=2999999;
struct  node
{
    ll  l,x,id;
}hash[1100000];ll  last[k],len;
ll  a[1100000],n,ans;
inline  void  add(ll  x,ll  id)//添加
{
    ll  y=x;x%=k;
    hash[++len].id=id;hash[len].x=y;hash[len].l=last[x];last[x]=len;
}
inline  ll  find(ll  x)//返回重复的ID
{
    ll  y=x;x%=k;
    for(ll  i=last[x];i;i=hash[i].l)
    {
        if(hash[i].x==y)return  hash[i].id;
    }
    return  0;//没有返回0
}
inline  void  del(ll  x,ll  id)//删除编号为id的
{
    x%=k;
    if(last[x]==id)last[x]=hash[id].l;//如果是最后一个插入的
    else//否则像单向链表一样删除
    {
        ll  pre=last[x];//之前的节点
        for(int  i=hash[pre].l;i;i=hash[i].l)//枚举那个节点被删除
        {
            if(hash[i].id==id)//判断
            {
                hash[pre].l=hash[i].l;
                return  ;
            }
            pre=i;//更新
        }
    }
}
inline  ll  mymax(ll  x,ll  y){return  x>y?x:y;}//取最大
int  main()
{
    scanf("%lld",&n);
    for(ll  i=1;i<=n;i++)scanf("%lld",&a[i]);//输入
    ll  l=1;
    for(ll  i=1;i<=n;i++)
    {
        ll  tkk=find(a[i]);//找到重复的位置
        for(;l<=tkk;l++)del(a[l],l);//删除
        add(a[i],i);//添加
        ans=mymax(ans,i-l+1);//记录
    }
    printf("%lld\n",ans);
    return  0;
}

终于在今天做完哈希和哈希表这一章节了。

注意事项

在设置指数时不能有两个位置重复,例如在二维hash中,如果把\((i,j)\)的指数设置成了\(i+j\),那么\((1,2)\)\((2,1)\)的位置可以互通,就不能保证数字的唯一性了。

同时参数的设置最好比数域要大,字符串的Hash,\(b\)就要比'z'要大,不然举个例子,数域是在\(1-9\)的,我设置一个\(3\),那是不是以为着\(1\)号位的\(9\)可以看作\(2\)号位的\(3\),模数就不说了,你设置这么小能起到判重的作用吗?

posted @ 2018-11-08 21:38  敌敌畏58  阅读(172)  评论(0编辑  收藏  举报