好玩的哈希
哈希
哈希这种东西还是挺好玩的。先说一说他最基本的作用:加入你想要 \(\mathcal O(1)\) 判断两个字符串或者其他东西是否相等,那么就用哈希吧。
哈希通俗的来讲,就是将一些超级大的数,映射到比较小的值域之中。但这势必会出现这样的问题:存在两个数,他们的哈希值相同,但本身不同,在判断的时候就死了。这就叫做 哈希冲突。
注意,只有离散化这种哈希永远不会出现哈希冲突!!
用到哈希的主要有两种好处。
- 数字化。我们可以把一些复杂抽象的东西变为数字化的可以存储的东西。这样也就更好的比较。
- 避免出现 \(\log\)。
具体的我们可以看几道题。
P3498 [POI 2010] KOR-Beads
首先一眼想到 \(\mathcal O(n\ln n)\)。这是个经典结论,可以给一个简单的证明。
\(\sum\limits_{i=1}^n\frac{n}{i}=n\int_0^n \frac{1}{n}=n\ln n\)。
然后就做完了。关键在于如何截取一段字符串,如果一个字符一个字符的加入,那么就会获得 \(\mathcal O(n^2\log n\ln n)\),\(\log\) 是 set 复杂度。
考虑用哈希。我们用哈希预处理这一段字符串,这样就可以去掉一个 \(n\)!!!!,复杂度 \(\mathcal O(n\log n\ln n)\)。
P2757 [国家集训队] 等差子序列
这道题非常地好。我来讲讲我们是如何才能一步一步地想到正解的。
首先,看到题目中说 \(Len\ge 3\),那么我们就可以无耻的去掉 \(Len>3\) 的情况,只考虑 \(Len=3\) 的情况。毕竟如果这都不行的话,谈何大于 \(3\) 的呢?
接着,我们就有了一个很朴素的思路。假设值域为 \(3\),那么我们就考虑做特殊的:中间值。
我们枚举中间值的下标 \(i\),那么怎么判断是否合法呢?
- 枚举左边的值的位置。
这个想法绝对朴素。我们以 \(a_i\) 为中间值,枚举 \(a_j,1\le j\le i-1\),然后去找是否存在 \(a_k,i+1\le k\le n\),使得 \(a_k+a_j=2_ai\)。这样复杂度是 \(\mathcal O(n^3)\)。考虑优化到极致,我们可以用一个桶去记录 \(i+1\) 到 \(n\) 的所有值。然后这样我们就可以去掉一个 \(n\),这样我们的复杂度极致即为 \(\mathcal O(n^2)\)。
那么我们怎么才能摆脱平方呢? - 关键:不去枚举左边的值的位置,直接在值域上思考。
显然发现,枚举位置完全无法继续优化了。这个时候就要去挖掘题目性质:\(a_1,\cdots,a_n\) 是 \(1\) 到 \(n\) 的一种排列。
那么怎么利用这一种性质呢?
我们可以举个例子,比如你枚举的是 \(5\),那么有哪些是可以的呢?显然就是 \(1,9\)、\(2,8\)、\(3,7\)、\(4,6\)。
那么怎么判断是否合法呢?由于我们中间值的下标是从小到大枚举的,所以我们可以利用这种性质!
我们给之前枚举过的值打上标记 \(1\),然后我们发现,如果关于 \(5\) 对称的数,也就是 \(1,9\)、\(2,8\)、\(3,7\)、\(4,6\),他们每一对的标记都相同,那么就表明他们,他们要么都没遍历过,也就是他们在 \(5\) 的同一侧!
然后就做完了。假设以 \(a_i\) 为中心的左右两端的 \(0,1\) 标记完全对等,那么就说明不合法,否则合法。这也就是判回文。
然后线段树判是否回文即可。期望复杂度为 \(\mathcal O(n\log n)\),显然可以通过。
bzoj#P3097. Hash Killer I
这道题是一道构造题,写完这道题可以锻炼人类智慧。
我们假设你有一个字符串 \(S_i\),表示级别 \(i\) 的字符串。那么我们定义 \(\overline{S_i}\) 为 \(S_i\) 的反串。
假设 \(S_1\) 为 \(ab\),那么 \(\overline{S_1}=ba\)。
然后来进行神之一笔。我们另 \(S_i=S_{i-1}+\overline{S_{i-1}}\)。
设 \(H1_i\) 计算 \(S_i\) 的哈希值,设 \(H2_i\) 计算 \(\overline{S_i}\)。
那么我们就会发现,按照题目中的哈希计算方法,就会有这种情况。
然后这道题就快做完了。我们假设可以 \(H1_i=H2_j\),那么两个哈希值做减法,假设是 \(f_i\),那么:
我们展开一下:
然后显然发现假设 \(base\) 为奇数,那么我们 \(f_i\) 就会有 \(2^{i(i-1)}\) 这个因子。
然后这道题就做完了。当我们取 \(i\ge 12\),即可通过本题!
6233. Hash killer V
这道题更唐。我们取 mo 的最小公倍数,此时他摸任何 \(mo_i\) 都为 \(0\),然后就构造完了。然后把这个哈希值转为26进制就行。
CF580E Kefa and Watch
这道题是这个样子的。看到第一个操作一眼想到线段树,主要的问题是第二个询问怎么处理。
最暴力的做法就是先去截取 \([l,l+x-1]\) 这一段字符串,然后一个一个拼接,最后看和线段树里的哈希值是否相同。
但是这样是 \(\mathcal O(n^2)\),考虑优化。
首先看到哈希想到哈希满足可加性,其次发现我们可以对他进行二进制拆分,即我们可以用倍增优化。
这样这道题就是 \(\mathcal O(n\log ^2n)\) 的。可以通过这道题。
解释:\(\mathcal O(n\log ^2n)\) 是因为线段树 pushdown 操作时也要倍增。
注意:CF 卡自然溢出!!!。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,BASE=131,MOD=998244353;
string c;
int n,m,k,f[20],P[N<<4];
struct SGT
{
int sum[N<<2],tag[N<<2];
void pushup(int x,int l,int r)
{
int mid=(l+r)>>1;
sum[x]=(sum[x<<1]*P[r-mid]%MOD+sum[x<<1|1])%MOD;
}
void pushdown(int x,int l,int r)
{
if(tag[x]==-1) return;
int mid=(l+r)>>1;
tag[x<<1]=tag[x<<1|1]=tag[x];
f[0]=tag[x];
for(int i=1,j=1;i<=18;i++,j<<=1)
f[i]=(f[i-1]*P[j]%MOD+f[i-1])%MOD;
int len=mid-l+1,cnt=0;
for(int i=18;~i;i--)
{
int p=1<<i;
if(len<p) continue;
cnt=(cnt+f[i]*P[len-p]%MOD)%MOD;
len-=p;
}
sum[x<<1]=cnt;
len=r-mid,cnt=0;
for(int i=18;~i;i--)
{
int p=1<<i;
if(len<p) continue;
cnt=(cnt+f[i]*P[len-p]%MOD)%MOD;
len-=p;
}
sum[x<<1|1]=cnt;
tag[x]=-1;
}
void build(int x,int l,int r)
{
tag[x]=-1;
if(l==r)
{
sum[x]=c[l]-'0';
return;
}
int mid=(l+r)>>1;
build(x<<1,l,mid),build(x<<1|1,mid+1,r);
pushup(x,l,r);
}
void upd(int x,int l,int r,int ql,int qr,int c)
{
if(ql<=l&&r<=qr)
{
int len=r-l+1,cnt=0;
f[0]=c,tag[x]=c;
for(int i=1,j=1;i<=18;i++,j<<=1)
f[i]=(f[i-1]*P[j]%MOD+f[i-1])%MOD;
for(int i=18;~i;i--)
{
int p=1<<i;
if(len<p) continue;
cnt=(cnt+f[i]*P[len-p]%MOD)%MOD;
len-=p;
}
sum[x]=cnt;
return ;
}
int mid=(l+r)>>1;pushdown(x,l,r);
if(ql<=mid) upd(x<<1,l,mid,ql,qr,c);
if(qr>mid) upd(x<<1|1,mid+1,r,ql,qr,c);
pushup(x,l,r);
}
int query(int x,int l,int r,int ql,int qr)
{
if(ql>qr) return 0;
if(ql<=l&&r<=qr) return sum[x];
pushdown(x,l,r);int mid=(l+r)>>1;
if(ql>mid) return query(x<<1|1,mid+1,r,ql,qr);
else if(qr<=mid) return query(x<<1,l,mid,ql,qr);
return (query(x<<1,l,mid,ql,mid)*P[qr-mid]%MOD+query(x<<1|1,mid+1,r,mid+1,qr))%MOD;
}
}T;
signed main()
{
cin.tie(0)->sync_with_stdio(0);
cout.tie(0)->sync_with_stdio(0);
cin>>n>>m>>k>>c;c=" "+c;
P[0]=1;
for(int i=1;i<=n*2;i++) P[i]=P[i-1]*BASE%MOD;
T.build(1,1,n);
int q=m+k;
for(;q;q--)
{
int op,l,r,x;
cin>>op>>l>>r>>x;
if(op==1) T.upd(1,1,n,l,r,x);
else
{
int len=r-l+1;
if(len<x)
{
cout<<"YES\n";
continue;
}
int cnt=T.query(1,1,n,l,l+(len%x)-1);
f[0]=T.query(1,1,n,l,l+x-1);
for(int i=1,j=1;i<=__lg(len/x);i++,j<<=1)
f[i]=(f[i-1]*P[j*x]%MOD+f[i-1])%MOD;
int sum=0;len=(len/x)*x;
for(int i=__lg(len/x);~i;i--)
{
int p=(1<<i)*x;
if(len<p) continue;
sum=(sum+f[i]*P[len-p]%MOD)%MOD;
len-=p;
}
len=r-l+1;sum=(sum*P[len%x]%MOD+cnt)%MOD;
if(sum==T.query(1,1,n,l,r))
cout<<"YES\n";
else
cout<<"NO\n";
}
}
return 0;
}
ABC398F
这种题简单说说就行了。首先一个关键的观察就是我们希望他的字符串尽量短,也就是这个 \(S\) 和它的反串 \(S'\) 尽量有公共前缀。
也就是说我们希望找到 \(S\) 的最大后缀回文串。
这道题做完了。注意,ABC 也不能自然溢出!!!*。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e6+5,BASE=13331,MOD=998244353;
int hs1[N],hs2[N],n,P[N];
string s;
int cal(const int &x,const int &y)
{
if(x<y) return x-y+MOD;
return x-y;
}
signed main()
{
cin>>s;n=s.size();
P[0]=1;
for(int i=1;i<=n;i++)
P[i]=P[i-1]*BASE%MOD;
for(int i=1;i<=n;i++)
hs1[i]=(hs1[i-1]*BASE%MOD+(s[i-1]-'A'))%MOD;
for(int i=n;i;i--)
hs2[i]=(hs2[i+1]*BASE%MOD+(s[i-1]-'A'))%MOD;
int id;
for(int i=1;i<=n;i++)
if(cal(hs1[n],hs1[i-1]*P[n-i+1]%MOD)==hs2[i])
{
id=i;
break;
}
if(id!=1)
{
string ss=s.substr(0,id-1);
reverse(ss.begin(),ss.end());
s+=ss;
}
cout<<s;
return 0;
}

浙公网安备 33010602011771号