Bootstrap

哈希Hash

哈希本质上是一种映射。

下面讲述三种哈希的应用。(参考了超级无敌牛逼帅气大学长 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–mn1)=(mn)!×mnm! ,那么冲突的概率就是 1 − m ! ( m − n ) ! m n 1− \frac{m!}{(m−n)!m^n} 1(mn)!mnm!

其实如果只是解决问题,但哈希就可以了,若是想要减小冲突概率,也可以使用双哈希。

例题:洛谷P3370 【模板】字符串哈希

link

序列或者字符串的哈希一般都是按照进制数来构造,形如 a + b ∗ p + c ∗ p 2 + … a+b∗p+c∗p^2+… a+bp+cp2+
哈希本身的计算的 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,...,Tm1,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 【模板】树同构

link

大致题意
m m m 棵无根树,每棵树有 n n n 个结点,要判断任意两棵树是否同构。
“同构” 的意思:若把一棵树的所有点重新标号,使得这棵树和另一棵树完全相同,那么这两个树是同构的,即它们具有相同的形态。

  • 1 ≤ n , m ≤ 50 1 \le n,m \le 50 1n,m50

思路
有了字符串哈希匹配得启发,可以考虑是否能将每棵树转化成一个哈希值,哈希值相同的树即是同构树。
因为父节点哈希值与所有子节点相关,但与子节点顺序无关,所以我们需要用 具有交换律 的运算来构造哈希函数 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=1soncnt(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 1n5×105

思路
实际上就是问是否能得出长度为 3 3 3 的序列为等差数列。

可以知道,若三个数的等差数列为 a , b , c a,b,c a,b,c ,那么有 ∣ b − a ∣ = ∣ c − b ∣ |b-a| = |c-b| ba=cb 。那么这个等差数列可以变为 a i − k , a i , a i + k a_i-k , a_i, a_i+k aik,ai,ai+k。又因为给出的序列是 1~n 的排列,所以可以从左到右枚举位置 i i i,即枚举中间项,如果 a i − k a_i-k aik 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 aik 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 独钓寒江雪

link

题意
给定一棵无根树,求其中本质不同的独立集的个数。
(即可以理解为:给定一棵 n n n 个节点的无根树,对该树的节点进行黑白染色,要求任意两个黑点没有直接的边相连,求本质不同的染色方案模 1 0 9 + 7 10^9+7 109+7 的余数。本质不同相当于树同构。)

  • n ≤ 5 × 1 0 5 n \le 5 \times 10^5 n5×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=vson(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=vson(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+n1n 。所以当不选结点 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=vson(u)Cfv,0+fv,1+n1n ;当选择结点 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=vson(u)Cfv,0+n1n

最后回归原题。即:(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+212+fu,0×fv,1若子树u和子树v不同构若子树u和子树v同构
    。(其中 C f u , 0 + 2 − 1 2 C_{f_{u,0}+2-1}^{2} Cfu,0+212 相当于上面的 C m + n − 1 n C_{m+n-1}^{n} Cm+n1n

代码
注意:由于上面的 C m + n − 1 n C_{m+n-1}^{n} Cm+n1n m + n − 1 m+n-1 m+n1 可能很大,而 n n n 只会很小,根据 C a b = a ! b ! ( a − b ) ! C_{a}^{b} = \frac{a!}{b!(a-b)!} Cab=b!(ab)!a! ,我们可以先求出 b ! b! b! 的逆元,然后 a ! ( a − b ) ! \frac{a!}{(a-b)!} (ab)!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] 星战

link

题意
有一个 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 1n,m,q5×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 由乃与大母神原型和偶像崇拜

link

题意
给你一个长为 n n n 的序列 a a a,每次两个操作:

  1. 修改 x x x 位置的值为 y y y
  2. 查询区间 [ l , r ] [l,r] [l,r] 是否可以重排为值域上连续的一段
  • n , m ≤ 5 × 1 0 5 n,m \le 5 \times 10^5 n,m5×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

如果有字符串,树,图等之间需进行比较,可以考虑使用哈希将他们映射到整数上就可以进行快速比较了。

;