目录
哈希本质上是一种映射。
下面讲述三种哈希的应用。(参考了超级无敌牛逼帅气大学长 l b y lby lby 的课, s m sm sm冬令营 D a y 1 Day1 Day1)
哈希的应用
(一)字符串哈希
字符串匹配,一般会与二分相结合(例如询问 L C P LCP LCP )。
介绍
对于字符串上的哈希来说,就是将字符串通过某个哈希函数将其映射为整数。当字符串很多时,就很有可能会出现两个(多个)字符串映射到的哈希值相同,这就是哈希冲突。但是将字符串映射为整数后,他们之间的比较效率就会快很多。
所以哈希就是牺牲一定的准确性,换来更高的效率或可行性。
由于有时字符串映射成的整数会非常巨大,所以一般会让最终的整数对一个大质数 p p p 取模,那么在字符串随机生成的情况下,取模后的数可以近似视为随机出现。冲突的概率就是 1 p \frac{1}{p} p1 了,非常小!但也是有的,如 “生日悖论” 。
哈希冲突概率
设 h a s h hash hash值 的集合大小是 m m m,原像集合大小是 n n n ,不冲突的概率是 1 × ( 1 – 1 m ) × … × ( 1 – n − 1 m ) = m ! ( m − n ) ! × m n 1 \times (1 – \frac{1}{m}) \times … \times (1 – \frac{n-1}{m}) = \frac{m!}{(m-n)! \times m^n} 1×(1–m1)×…×(1–mn−1)=(m−n)!×mnm! ,那么冲突的概率就是 1 − m ! ( m − n ) ! m n 1− \frac{m!}{(m−n)!m^n} 1−(m−n)!mnm! 。
其实如果只是解决问题,但哈希就可以了,若是想要减小冲突概率,也可以使用双哈希。
例题:洛谷P3370 【模板】字符串哈希
序列或者字符串的哈希一般都是按照进制数来构造,形如
a
+
b
∗
p
+
c
∗
p
2
+
…
a+b∗p+c∗p^2+…
a+b∗p+c∗p2+… 。
哈希本身的计算的
O
(
n
)
O(n)
O(n) 的,重复利用时就很高效,可达到
O
(
1
)
O(1)
O(1)。
(二)哈希表
可以理解为 unordered_map ,有多种实现方式,常用的是线性探查法和独立链法。
线性勘查法
首先建立一个长为 n n n 的数组 T T T,每个位置存储一对信息 ( x , v ) (x,v) (x,v) 。
插入 ( x , v ) (x,v) (x,v) 时,找到 x x x 对应的哈希值 y = h a s h ( x ) y = hash(x) y=hash(x) ,如 果 T y T_y Ty 为空,就将 ( x , v ) (x, v) (x,v) 放入 T y T_y Ty 位置;否则就从位置 T y T_y Ty 往后扫描位置 T y + 1 , T y + 2 , . . . , T m − 1 , T 0 , T 1 , . . . T_{y+1}, T_{y+2}, . . . , T{m−1}, T_0, T_1, . . . Ty+1,Ty+2,...,Tm−1,T0,T1,...,找到第一个未被占用的位置,将 ( x , v ) (x, v) (x,v) 放入这个位置。
修改、删除和查询操作时,从位置 T y T_y Ty 开始向后扫描,直到找到一个位置的值等于 v v v,然后对这个位置进行操作即可。
独立链法
对每个哈希值建立一个初始为空的链表。
执行有关 x 的操作时,就对 y = h a s h ( x ) y = hash(x) y=hash(x) 位置的链表进行操作。记 y 对应的链表的大小为 c y c_y cy ,那么这次操作中访问的链表元素个数 ≤ c y \le c_y ≤cy。
(三)树哈希
以一道题目讲述。
例题:洛谷P5043 【模板】树同构
大致题意
有
m
m
m 棵无根树,每棵树有
n
n
n 个结点,要判断任意两棵树是否同构。
“同构” 的意思:若把一棵树的所有点重新标号,使得这棵树和另一棵树完全相同,那么这两个树是同构的,即它们具有相同的形态。
- 1 ≤ n , m ≤ 50 1 \le n,m \le 50 1≤n,m≤50
思路
有了字符串哈希匹配得启发,可以考虑是否能将每棵树转化成一个哈希值,哈希值相同的树即是同构树。
因为父节点哈希值与所有子节点相关,但与子节点顺序无关,所以我们需要用 具有交换律
的运算来构造哈希函数
h
a
s
h
(
)
hash()
hash(),容易想到的是
h
a
s
h
(
u
)
=
∑
h
a
s
h
(
v
)
hash(u)= \sum hash(v)
hash(u)=∑hash(v) 或者
h
a
s
h
(
u
)
=
∏
h
a
s
h
(
v
)
hash(u)= \prod ℎash(v)
hash(u)=∏hash(v) (
v
v
v 为
u
u
u 的子结点)。但是这样冲突概率较大。
可以将二者结合,加入更多每棵树都有可能不同的元素到
h
a
s
h
hash
hash函数 之中。比如:(
p
1
p_1
p1,
p
2
p_2
p2 ,以及
p
p
p 均为质数,
s
o
n
c
n
t
(
u
)
soncnt(u)
soncnt(u) 为
u
u
u 的儿子数量,
s
i
z
e
(
u
)
size(u)
size(u) 为
u
u
u 的子树大小)
h
a
s
h
(
u
)
=
p
1
+
p
2
s
o
n
c
n
t
(
u
)
∏
h
a
s
h
(
v
)
hash(u) = p_1+p_2^{soncnt(u)} \prod ℎash(v)
hash(u)=p1+p2soncnt(u)∏hash(v)
或者,(
h
a
s
h
(
v
)
hash(v)
hash(v) 按照从小到大排序)但是我试过好像下面这种不太对,我也不知道什么原因
h
a
s
h
(
u
)
=
∑
i
=
1
s
o
n
c
n
t
(
u
)
h
a
s
h
(
v
)
×
p
i
+
s
i
z
e
(
u
)
hash(u) = \sum_{i=1}^{soncnt(u)} ℎash(v) \times p^i + size(u)
hash(u)=i=1∑soncnt(u)hash(v)×pi+size(u)
当然还有很多方法。这里补充一种 新方法 。(但是这种好像被卡掉过,但是了解一下还是可以的)
还有一个问题 —— 现在是无根树, h a s h hash hash值 是在有根树上求的,那么就要考虑 无根树 如何转化为 有根树 。
有一个想法是对于树上的每个点分别以他为根求一个哈希值,显然会超时。其实只需要以树的重心为根即可,当然有的树会有两个重心,那就记录两个哈希值,比较的时候看两个哈希值是否都对应的上就好。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=55;
const ll p=2333,mod=1e9+7;
int idx,head[maxn];
struct EDGE{ int v,next; }e[maxn*2];
void Insert(int u,int v){
e[++idx]={v,head[u]};
head[u]=idx;
}
int n,misz,root,nroot,siz[maxn];
void Find_rt(int x,int fa){//找树的重心 (root,nroot)
siz[x]=1;
int mas=0;
for(int i=head[x];i;i=e[i].next){
int v=e[i].v;
if(v!=fa){
Find_rt(v,x);
siz[x]+=siz[v],mas=max(mas,siz[v]);
}
}
mas=max(mas,n-siz[x]);
if(mas<misz) misz=mas,root=x,nroot=0;//nroot也要清零
else if(mas==misz) nroot=x;
}
ll hs[maxn],pw[maxn];
void Dfs(int x,int fa){//计算哈希值 hs(u) = p1* ∏ hs(v) + p2^soncnt(u)
siz[x]=1;
int son=0; ll sum=1;
for(int i=head[x];i;i=e[i].next){
int v=e[i].v;
if(v!=fa){
Dfs(v,x);
(sum*=hs[v])%=mod,son++;
}
}
hs[x]=(131*pw[son]%mod+sum)%mod;
}
struct NODE{ ll rths,nrths; }f[maxn];
bool operator == (NODE a,NODE b){ return a.rths==b.rths && a.nrths==b.nrths; }
int main(){
pw[0]=1;
for(int i=1;i<=50;i++)
pw[i]=pw[i-1]*p%mod;
int m; cin>>m;
for(int i=1;i<=m;i++){
cin>>n;
for(int j=1;j<=n;j++){
int x; cin>>x;
if(x) Insert(x,j),Insert(j,x);
}
misz=1e9,root=nroot=0;
Find_rt(1,0);//求出树的重心
Dfs(root,0),f[i].rths=hs[root];//算树的 hash值
if(nroot) Dfs(nroot,0),f[i].nrths=hs[nroot];
if(f[i].rths>f[i].nrths) swap(f[i].rths,f[i].nrths);
idx=0;
for(int j=1;j<=n;j++)
head[j]=siz[i]=hs[i]=0;//!
}
for(int i=1;i<=m;i++)
for(int j=1;j<=i;j++)
if(f[i]==f[j]){ cout<<j<<"\n"; break; }
return 0;
}
一些关于哈希的练习
1. 洛谷P2757 [国家集训队] 等差子序列
link
题意
给出一个 1~n 的排列,在其中按顺序选择若干个数(
≥
3
\ge 3
≥3),可以不连续,问是否能使得出的序列为一个等差数列。
- 1 ≤ n ≤ 5 × 1 0 5 1 \le n \le 5 \times 10^5 1≤n≤5×105
思路
实际上就是问是否能得出长度为
3
3
3 的序列为等差数列。
可以知道,若三个数的等差数列为 a , b , c a,b,c a,b,c ,那么有 ∣ b − a ∣ = ∣ c − b ∣ |b-a| = |c-b| ∣b−a∣=∣c−b∣ 。那么这个等差数列可以变为 a i − k , a i , a i + k a_i-k , a_i, a_i+k ai−k,ai,ai+k。又因为给出的序列是 1~n 的排列,所以可以从左到右枚举位置 i i i,即枚举中间项,如果 a i − k a_i-k ai−k 和 a i + k a_i+k ai+k 这两个数一个出现在位置 i i i 之前,一个在位置 i i i 之后,那么就能判定答案为存在。
按照这个思路,建立了一个数组,初始全为 0 0 0,扫过一个数 a i a_i ai,就把数组下标为 a i a_i ai 的位置标为 1 1 1。如果我们以位置 a i a_i ai 为中心“对折”这个数组,若对于所有重合的位置,重合的两个数相等(即均为 0 0 0 或 1 1 1),则说明对于所有合法的 a i − k a_i-k ai−k 和 a i + k a_i+k ai+k 必然同在 i i i 左侧或右侧(也就是不存在以 a i a_i ai 为中间项的等差数列)。发现这是一个判断回文的过程。
想到可以用字符串哈希解决,用线段树维护区间正哈希和反序列哈希。“对折”的时候就是提取 从中间项到左端点的区间 以及 从中间项到右端点的区间 进行比较,即 [ 左端点 , 中间项 ] [左端点,中间项] [左端点,中间项] 的反序列的哈希值 和 [ 中间项 , 右端点 ] [中间项,右端点] [中间项,右端点] 的正序列的哈希值 进行比较,不相同就是可以形成等差数列。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e5+5;
const ll P=131,mod=1e9+7;
ll p[maxn];
struct NODE{ ll hs,fhs; }tree[maxn*4];//记录一个正哈希和一个将序列反转后的哈希
void Pushup(int rt,int l,int r){
int mid=l+r>>1,ls=rt*2,rs=ls+1;
tree[rt].hs=(tree[ls].hs*p[r-mid]%mod+tree[rs].hs)%mod;
tree[rt].fhs=(tree[ls].fhs+tree[rs].fhs*p[mid-l+1]%mod)%mod;
}
void Modify(int rt,int l,int r,int x){
if(l==r){
tree[rt].hs=tree[rt].fhs=1;
return;
}
int mid=l+r>>1;
if(x<=mid) Modify(rt*2,l,mid,x);
else Modify(rt*2+1,mid+1,r,x);
Pushup(rt,l,r);
}
int w;
ll xhs;
void Query_hs(int rt,int l,int r,int x,int y){
if(x<=l&&r<=y){
xhs=(xhs+tree[rt].hs*p[w]%mod)%mod;
w+=(r-l+1);
return;
}
int mid=l+r>>1;
if(y>mid) Query_hs(rt*2+1,mid+1,r,x,y);//!
if(x<=mid) Query_hs(rt*2,l,mid,x,y);
}
ll yhs;
void Query_fhs(int rt,int l,int r,int x,int y){
if(x<=l&&r<=y){
yhs=(yhs+tree[rt].fhs*p[w]%mod)%mod;
w+=(r-l+1);
return;
}
int mid=l+r>>1;
if(x<=mid) Query_fhs(rt*2,l,mid,x,y);
if(y>mid) Query_fhs(rt*2+1,mid+1,r,x,y);
}
int a[maxn];
int main(){
p[0]=1;
for(int i=1;i<=5e5;i++)
p[i]=p[i-1]*P%mod;
int t; cin>>t;
while(t--){
int n; cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
bool ok=0;
for(int i=1;i<=n;i++){
Modify(1,1,n,a[i]);
if(a[i]==1||a[i]==n) continue;
int len=min(a[i],n-a[i]+1);
xhs=w=0; Query_hs(1,1,n,a[i]-len+1,a[i]+len-1);
yhs=w=0; Query_fhs(1,1,n,a[i]-len+1,a[i]+len-1);
if(xhs!=yhs){ ok=1; break; }
}
cout<<(ok?'Y':'N')<<"\n";
for(int i=1;i<=4*n;i++)
tree[i].hs=tree[i].fhs=0;
}
return 0;
}
2. 洛谷P4895 独钓寒江雪
题意
给定一棵无根树,求其中本质不同的独立集的个数。
(即可以理解为:给定一棵
n
n
n 个节点的无根树,对该树的节点进行黑白染色,要求任意两个黑点没有直接的边相连,求本质不同的染色方案模
1
0
9
+
7
10^9+7
109+7 的余数。本质不同相当于树同构。)
- n ≤ 5 × 1 0 5 n \le 5 \times 10^5 n≤5×105
思路
先考虑如果树有根且不需要“本质不同”。即:(1)一棵 有 根树,求 不同 的独立集个数。
这就是“没有上司的舞会”这题,设树上
D
P
DP
DP 状态
f
u
,
0
/
1
f_{u,0/1}
fu,0/1 表示在以
u
u
u 为根的子树内,结点
u
u
u 取或不取的方案数。转移很显然。
f
u
,
0
=
∏
v
∈
s
o
n
(
u
)
(
f
v
,
0
+
f
v
,
1
)
f_{u,0} = \prod_{v \in son(u)} (f_{v,0} + f_{v,1})
fu,0=∏v∈son(u)(fv,0+fv,1) ,
f
u
,
1
=
∏
v
∈
s
o
n
(
u
)
f
v
,
0
f_{u,1} = \prod_{v \in son(u)} f_{v,0}
fu,1=∏v∈son(u)fv,0
然后再考虑树有根但是要“本质不同”。即:(2)一棵 有 根树,求 本质不同 的独立集个数。
转移状态依然可以这么设,但是方程当然就不能直接乘起来了,因为对于一个以
u
u
u 为根的树,他的子树
v
v
v 有可能是同构的。
假设同构的子树有
n
n
n 个,每棵树可以选择“黑白涂色方案”中的一种,共m种,子树之间无序,则方案数为
C
m
+
n
−
1
n
C_{m+n-1}^{n}
Cm+n−1n 。所以当不选结点
u
u
u 时,
f
u
,
0
=
∏
v
∈
s
o
n
(
u
)
C
f
v
,
0
+
f
v
,
1
+
n
−
1
n
f_{u,0} = \prod_{v \in son(u)} C_{f_{v,0}+f_{v,1}+n-1}^{n}
fu,0=∏v∈son(u)Cfv,0+fv,1+n−1n ;当选择结点
u
u
u 时,
f
u
,
1
=
∏
v
∈
s
o
n
(
u
)
C
f
v
,
0
+
n
−
1
n
f_{u,1} = \prod_{v \in son(u)} C_{f_{v,0}+n-1}^{n}
fu,1=∏v∈son(u)Cfv,0+n−1n
最后回归原题。即:(3)一棵 无 根树,求 本质不同 的独立集个数。
一般将无根树转成有根树,都是以树的重心为根。
- 当重心只有一个时,即变成
问题(2)
求解, a n s = f r o o t , 0 + f r o o t , 1 ans=f_{root,0} + f_{root,1} ans=froot,0+froot,1 ; - 当重心有两个时,设他们分别为
u
u
u ,
v
v
v ,则可以先断开边
(
u
,
v
)
(u,v)
(u,v) ,原问题就变成对两棵有根树求解,即可以看成两个
问题(2)
。
a n s = { f u , 0 × f v , 0 + f u , 0 × f v , 1 + f u , 1 × f v , 0 若子树 u 和子树 v 不同构 C f u , 0 + 2 − 1 2 + f u , 0 × f v , 1 若子树 u 和子树 v 同构 ans = \begin{cases} f_{u,0} \times f_{v,0} + f_{u,0} \times f_{v,1} + f_{u,1} \times f_{v,0}& 若子树u 和子树v 不同构 \\ C_{f_{u,0}+2-1}^{2} + f_{u,0} \times f_{v,1} & 若子树u 和子树v 同构 \end{cases} ans={fu,0×fv,0+fu,0×fv,1+fu,1×fv,0Cfu,0+2−12+fu,0×fv,1若子树u和子树v不同构若子树u和子树v同构
。(其中 C f u , 0 + 2 − 1 2 C_{f_{u,0}+2-1}^{2} Cfu,0+2−12 相当于上面的 C m + n − 1 n C_{m+n-1}^{n} Cm+n−1n )
代码
注意:由于上面的
C
m
+
n
−
1
n
C_{m+n-1}^{n}
Cm+n−1n 中
m
+
n
−
1
m+n-1
m+n−1 可能很大,而
n
n
n 只会很小,根据
C
a
b
=
a
!
b
!
(
a
−
b
)
!
C_{a}^{b} = \frac{a!}{b!(a-b)!}
Cab=b!(a−b)!a! ,我们可以先求出
b
!
b!
b! 的逆元,然后
a
!
(
a
−
b
)
!
\frac{a!}{(a-b)!}
(a−b)!a! 暴力算即可。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e5+5,p=31,mod=1e9+7;
int idx,head[maxn];
struct EDGE{ int v,next; }e[maxn*2];
void Insert(int u,int v){
e[++idx]={v,head[u]};
head[u]=idx;
}
int n,misz,root,nroot,siz[maxn];
void Find_rt(int x,int fa){
siz[x]=1;
int ma=0;
for(int i=head[x];i;i=e[i].next){
int v=e[i].v;
if(v!=fa){
Find_rt(v,x);
siz[x]+=siz[v],ma=max(ma,siz[v]);
}
}
ma=max(ma,n-siz[x]);
if(ma<misz) misz=ma,root=x,nroot=0;
else if(ma==misz) nroot=x;
}
ll hs[maxn];
bool cmp(int a,int b){ return hs[a]<hs[b]; }
ll inv[maxn];
ll C(ll m,ll n){
ll s=1;
for(ll i=1;i<=n;i++)
(s*=(m-i+1)*inv[i]%mod)%=mod;
return s;
}
int b[maxn];
ll f[maxn][3],pw[maxn];
void Dfs(int x,int fa){
f[x][0]=f[x][1]=siz[x]=1; ll sum=19;
for(int i=head[x];i;i=e[i].next){
int v=e[i].v;
if(v!=fa){
Dfs(v,x);
siz[x]+=siz[v],(sum*=hs[v])%=mod;
}
}
int son=0;
for(int i=head[x];i;i=e[i].next){
int v=e[i].v;
if(v!=fa) b[++son]=v;
}
hs[x]=(131*pw[son]%mod+sum)%mod;
sort(b+1,b+son+1,cmp);
for(int i=1;i<=son;){
int v=b[i],j=i;
while(j<=son&&hs[b[j]]==hs[v]) j++;
j--;
(f[x][0]*=C(f[v][0]+f[v][1]+j-i,j-i+1))%=mod;
(f[x][1]*=C(f[v][0]+j-i,j-i+1))%=mod;
i=j+1;
}
}
ll Pow_(ll x,ll y){
ll s=1;
while(y){
if(y&1) (s*=x)%=mod;
(x*=x)%=mod;
y>>=1;
}
return s;
}
int main(){
cin>>n;
pw[0]=1;
for(int i=1;i<=n;i++){
pw[i]=pw[i-1]*p%mod;
inv[i]=Pow_(i,mod-2);
}
for(int i=1;i<n;i++){
int x,y; cin>>x>>y;
Insert(x,y);
Insert(y,x);
}
misz=1e9,root=nroot=0,Find_rt(1,0);
if(!nroot){
Dfs(root,0);
cout<<(f[root][0]+f[root][1])%mod;
}
else{
Dfs(root,nroot); Dfs(nroot,root);
if(hs[root]==hs[nroot]) cout<<(f[root][0]*f[nroot][1]%mod+C(f[root][0]+2-1,2))%mod;
else cout<<(f[root][0]*f[nroot][0]%mod+f[root][0]*f[nroot][1]%mod+f[root][1]*f[nroot][0]%mod)%mod;
}
return 0;
}
3. 洛谷P8819 [CSP-S 2022] 星战
题意
有一个
n
n
n 个点
m
m
m 条边的有向图,每条边就是一个虫洞。有四种操作:
- 删除一条边;
- 删除所有连向某个点的边;
- 增加一条边;
- 还原所有连向某个点的边;
共 q q q 次操作,问每次操作之后,图上所有点的出度是否都为 1 1 1 。
- 1 ≤ n , m , q ≤ 5 × 1 0 5 1 \le n,m,q \le 5 \times 10^5 1≤n,m,q≤5×105
思路
首先,考虑如何判读一个图是否所有点出度都为
1
1
1。有了树同构的经验,可以考虑是否能将一个图映射至一个哈希值,通过哈希值来判断图上的所有点的出度都为
1
1
1 。
发现如果通过边权构造哈希函数是很困难的,那考虑点权。
如果将每个点的所有入边所对应的点的点权之和叫做这个点的
h
a
s
h
hash
hash值,那么如果这个图上所有点的出度都为
1
1
1 ,则
图上所有点的
h
a
s
h
值之和
=
图上所有点的点权之和
图上所有点的 hash值之和 = 图上所有点的点权之和
图上所有点的hash值之和=图上所有点的点权之和。
那么先给每个点随机化一个点权,每次操作维护被操作点的 h a s h hash hash值 以及 所有点的 h a s h hash hash值之和即可。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e5+5,mod=1e9+7;
ll a[maxn],sum[maxn],b[maxn];
int main(){
int n,m; cin>>n>>m;
srand(time(NULL));
ll ans=0;
for(int i=1;i<=n;i++)
a[i]=rand()%mod,ans+=a[i];
for(int i=1;i<=m;i++){
int x,y; cin>>x>>y;
sum[y]+=a[x];
}
ll res=0;
for(int i=1;i<=n;i++)
res+=sum[i],b[i]=sum[i];
int q; cin>>q;
while(q--){
int t,x; cin>>t>>x;
if(t==1){
int y; cin>>y;
sum[y]-=a[x],res-=a[x];
}
else if(t==2) res-=sum[x],sum[x]=0;
else if(t==3){
int y; cin>>y;
sum[y]+=a[x],res+=a[x];
}
else res+=(b[x]-sum[x]),sum[x]=b[x];
cout<<(res==ans?"YES":"NO")<<"\n";
}
return 0;
}
4. 洛谷P3792 由乃与大母神原型和偶像崇拜
题意
给你一个长为
n
n
n 的序列
a
a
a,每次两个操作:
- 修改 x x x 位置的值为 y y y
- 查询区间 [ l , r ] [l,r] [l,r] 是否可以重排为值域上连续的一段
- n , m ≤ 5 × 1 0 5 n,m \le 5 \times 10^5 n,m≤5×105,初始值的值域小于 2.5 × 1 0 7 2.5\times 10^7 2.5×107,修改操作的 y y y 小于等于 n n n
思路
肯定是往哈希的方向想了。
如何判断一堆数是否能重排成连续一段,如果这堆数没有重复,那就只用记录这堆数的最大最小值即可;但是如果有重复的呢?如果记录这堆数的和,那还不足以判断,说明这个哈希函数越复杂越好。那平方和 (会被卡) ,立方和 (暂时还没被卡) ,或者
h
a
s
h
(
这堆数
)
=
∑
i
∈
这堆数的下标
a
i
a
i
hash(这堆数) = \sum_{i \in 这堆数的下标} a_i^{a_i}
hash(这堆数)=∑i∈这堆数的下标aiai (lby做法) …… 复杂点的大概都不会被卡。
总之弄个线段树之类的数据结构维护这个序列即可。
代码
#include<bits/stdc++.h>
#define ll __int128
using namespace std;
const int maxn=5e5+5;
struct TREE{ ll mi,ma,sum; }tree[maxn*4];
void Build(int rt,int l,int r){
tree[rt].mi=1e9;
if(l==r) return;
int mid=l+r>>1;
Build(rt*2,l,mid);
Build(rt*2+1,mid+1,r);
}
void Modify(int rt,int l,int r,int x,ll y){
if(l==r){
tree[rt]={y,y,y*y*y};
return;
}
int mid=l+r>>1;
if(x<=mid) Modify(rt*2,l,mid,x,y);
else Modify(rt*2+1,mid+1,r,x,y);
tree[rt].mi=min(tree[rt*2].mi,tree[rt*2+1].mi);
tree[rt].ma=max(tree[rt*2].ma,tree[rt*2+1].ma);
tree[rt].sum=tree[rt*2].sum+tree[rt*2+1].sum;
}
int Query_mi(int rt,int l,int r,int x,int y){
if(x<=l&&r<=y) return tree[rt].mi;
int mid=l+r>>1,s=1e9;
if(x<=mid) s=min(s,Query_mi(rt*2,l,mid,x,y));
if(y>mid) s=min(s,Query_mi(rt*2+1,mid+1,r,x,y));
return s;
}
int Query_ma(int rt,int l,int r,int x,int y){
if(x<=l&&r<=y) return tree[rt].ma;
int mid=l+r>>1,s=0;
if(x<=mid) s=max(s,Query_ma(rt*2,l,mid,x,y));
if(y>mid) s=max(s,Query_ma(rt*2+1,mid+1,r,x,y));
return s;
}
ll Query_sum(int rt,int l,int r,int x,int y){
if(x<=l&&r<=y) return tree[rt].sum;
int mid=l+r>>1; ll s=0;
if(x<=mid) s+=Query_sum(rt*2,l,mid,x,y);
if(y>mid) s+=Query_sum(rt*2+1,mid+1,r,x,y);
return s;
}
ll Sqr(ll n){//sum_{i=1}{n} i^3
return (1+n)*n/2*(1+n)*n/2;
}
ll a[maxn];
int main(){
int n,m; scanf("%d%d",&n,&m);
Build(1,1,n);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
Modify(1,1,n,i,a[i]);
}
while(m--){
int opt,x,y; scanf("%d%d%d",&opt,&x,&y);
if(opt==1) Modify(1,1,n,x,y);
else{
int mi=Query_mi(1,1,n,x,y),ma=Query_ma(1,1,n,x,y);
ll sum=Query_sum(1,1,n,x,y);
(Sqr(ma)-Sqr(mi-1)==sum?printf("damushen"):printf("yuanxing")),printf("\n");
}
}
return 0;
}
总结
尽量用些不寻常的质数作为 h a s h hash hash函数 中的系数以防被卡,记得考虑计算是否会越界。(开 long long 或 __int128)
如果有字符串,树,图等之间需进行比较,可以考虑使用哈希将他们映射到整数上就可以进行快速比较了。