万能的进制哈希
题外话:
为什么要学字符串算法?
为了快速比较两个字符串是否相等,众所周知垃圾C++在比较两个字符串的时候效率并不高,所以我们需要设计一种算法更高效地比较字符串
大致用途:
1.判断两个字符串是否相等;
2.判断一个字符串是否曾经出现过;
3.让某些用户口吐芬芳的时候网页可以自动屏蔽掉;
定义:
百度百科:Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。
人话翻译:把字符赋予进制和模数,将每一个字符串映射为一个小于模数数字。
具体操作:
我们设置进制(base)为131,模数(mod)为1e9+7,现在我们对一个字符串s进行哈希
char s[10];
cin>>(s+1);
int len=strlen(s+1);
int base=131,mod=1e9+7;
for(int i=1;i<=len;++i)
{
hash[i] = ( ( hash[i-1] * base ) + s[i] ) % mod ;
}
这样hash[len]里面就是字符串s的哈希值了;
hash还有一个方便的操作就是取子串的hash值
hash[l,r] = ( hash [r] - hash[l-1] * pw[r-l+1] ) %mod //伪代码 pw[r-l+1]为base的(r-l+1)次方
注意:
哈希冲突:
什么是哈希冲突:比如orz的哈希值是2333,然而sto的哈希值也是2333,这样就会产生哈希冲突,从而让哈希算法判断失误。
解决方法:
1.模数选取大质数
如果选取合数那么他的剩余系将会有所浪费(不了解剩余系请找一篇数论博客QwQ),如果质数过小将会导致剩余系过小,哈希冲突几率增大(质数过大爆负数,谨慎设置)
2.双模数哈希
我们可以通过设置两个不同的哈希方式,对于一个字符串,当且仅当两个哈希值都相同时才判定相当。
这种方法相当有效,除非出题人对着你的数据卡你,否则正确率近乎100%(详情请见BZOJ Hash Killer 3)
实战应用:
1.hash判断最长公共前缀
题目概述:给定一个字符串,要求维护两种操作在字符串中插入一个字符询问某两个位置开始的 LCP(最长公共前缀)插入操作次数
插入<=200,字符串总长度<=50000,查询次数<=20000。
分析:插入<=200,考虑每次插入暴力维护 复杂度200*50000
每次查询二分LCP的长度,然后hash O(1)判断是否相等
2.哈希判断回文串
求一个字符串中包含几个回文串?
manachar?其实哈希也很好用,而且复杂度只多一个log呢QwQ
对于该串维护正反两个哈希值,我们称为正向哈希和反向哈希
每次二分一个回文串长度,用正反哈希O(1)判断是否相等
对于奇偶回文串可以考虑每个字符中间插入一个新字符,也可以分开处理
3.线段树维护哈希
给出一个1到n的排列,问是否存在长度大于等于3的等差子序列
分析:其实只要找长度等于3的就好了嘛QwQ
一个01串,从前往后扫描这个序列,将扫描过的数字对应位置变为1
对于每一个数字,如果目前不能构成等差序列,那么他两侧的01串必然是一个回文串,我们可以对01串维护一个哈希值进行比较
由于我们需要动态修改和区间查询哈希值,所以我们考虑权值线段树来维护正反哈希。
线段树维护哈希细节较多,这里我细致地说一下,同时为了代码清晰可读,这里利用unsigned long long 自然溢出,略去取模操作
首先一些变量函数
ans1[500010]//正向哈希线段树节点 ans2[500010]//反向哈希线段树节点 query1函数:正向哈希查询 query2函数:反向哈希查询
线段树push_up向上维护操作
要将正哈希的左儿子的哈希值乘上进制的右区间长度次方,反哈希的右儿子乘上进制的左区间长度次方
ans1[p] = ans1[ls(p)] * pw[r-mid] + ans1[rs(p)] ;
ans2[p] = ans2[rs(p)] * pw[mid-l+1] + ans2[ls(p)] ;//注意ls(p)和rs(p)的区别
本题目不需要push_down下放操作,后面会提到
查询时分类讨论:
完全在左儿子区间:直接返回左儿子值;
完全在右儿子区间:直接返回右儿子值;
二者都在:
正向:左儿子值*右查询长度+右儿子值;
反向:右儿子值*左查询长度+左儿子值;
inline int query1(int tl,int tr,int l,int r,int p)
{
if(tl<=l&&r<=tr) return ans1[p];
if(tr<=mid) return query1(tl,tr,l,mid,ls(p));
else if(mid<tl) return query1(tl,tr,mid+1,r,rs(p));
else
{
int lx=query1(tl,tr,l,mid,ls(p));
int rx=query1(tl,tr,mid+1,r,rs(p));
return lx*pw[min(tr,r)-mid]+rx;
}
}
inline int query2(int tl,int tr,int l,int r,int p)
{
if(tl<=l&&r<=tr) return ans2[p];
if(tr<=mid) return query2(tl,tr,l,mid,ls(p));
else if(mid<tl) return query2(tl,tr,mid+1,r,rs(p));
else
{
int lx=query2(tl,tr,l,mid,ls(p));
int rx=query2(tl,tr,mid+1,r,rs(p));
return rx*pw[mid-max(tl,l)+1]+lx;
}
}
多测不清空,爆零两行泪
下面贴完整代码
#include<bits/stdc++.h>
using namespace std;
#define int unsigned long long
#define ls(p) (p<<1)
#define rs(p) (p<<1|1)
#define mid ((l+r)>>1)
inline int read()
{
int x=0,f=1;
char ch;
for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());
if(ch=='-') f=0,ch=getchar();
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return f?x:-x;
}
int T,base=131;
int a[100010],n;
int ans1[500010],ans2[500010];
int pw[100010];
bool flag;
inline void update(int tl,int tr,int l,int r,int p)
{
if(tl<=l&&r<=tr)
{
ans1[p]=ans2[p]=1;
return;
}
if(tl<=mid)
update(tl,tr,l,mid,ls(p));
else
update(tl,tr,mid+1,r,rs(p));
ans1[p] = ans1[ls(p)] * pw[r-mid] + ans1[rs(p)] ;
ans2[p] = ans2[rs(p)] * pw[mid-l+1] + ans2[ls(p)] ;
}
inline int query1(int tl,int tr,int l,int r,int p)
{
if(tl<=l&&r<=tr) return ans1[p];
if(tr<=mid) return query1(tl,tr,l,mid,ls(p));
else if(mid<tl) return query1(tl,tr,mid+1,r,rs(p));
else
{
int lx=query1(tl,tr,l,mid,ls(p));
int rx=query1(tl,tr,mid+1,r,rs(p));
return lx*pw[min(tr,r)-mid]+rx;
}
}
inline int query2(int tl,int tr,int l,int r,int p)
{
if(tl<=l&&r<=tr) return ans2[p];
if(tr<=mid) return query2(tl,tr,l,mid,ls(p));
else if(mid<tl) return query2(tl,tr,mid+1,r,rs(p));
else
{
int lx=query2(tl,tr,l,mid,ls(p));
int rx=query2(tl,tr,mid+1,r,rs(p));
return rx*pw[mid-max(tl,l)+1]+lx;
}
}
signed main()
{
T=read();
for(int i=pw[0]=1;i<=100000;++i)
pw[i] = pw[i-1] * base;
while(T--)
{
n=read();
flag=0;
memset(ans1,0,sizeof(ans1));
memset(ans2,0,sizeof(ans2));
for(int i=1;i<=n;++i)
{
a[i]=read();
if(!flag)
{
int d=min(a[i]-1,n-a[i]);
if(d)
{
if(query1(a[i]-d,a[i],1,n,1)^query2(a[i],a[i]+d,1,n,1))
flag=1;
}
update(a[i],a[i],1,n,1);
}
}
puts(flag?"Y":"N");
}
return 0;
}
4.哈希判断循环节
给定一个数字串,要求维护以下两个操作:
1.将l到r区间内数字全部改为k
2.询问l到r区间内是否存在长度为k的循环节
前置神仙结论:判断一个字符串[ l , r ] 是否有长度为k的循环节,只需判断 [ l+d , r ] 和 [ l , r-d ] 是否相等。
有了上述结论,这道题就变成了一个哈希值的区间修改和区间查询问题,再码一颗线段树就可以了
在这里放一下上面没有展示的push_down下放代码
inline void push_down(int l,int r,int p)
{
int k=tag[p];
ans[ls(p)] = val[k][mid-l+1]; // val[k][len] 预处理出来
ans[rs(p)] = val[k][r-mid]; //表示字符串内全部都是k的长度为len的 哈希值
tag[ls(p)]=tag[rs(p)]=tag[p];
tag[p]=-1; // 修改可能存在 0 所以tag 要赋成 -1
}
for(int i=0;i<10;++i)
{
for(int j=1;j<=100005;++j)
{
val[i][j]=val[i][j-1] * base + i;
}
}