1. 定义和哈希,又称集合哈希。
为什么叫集合哈希呢,集合与序列的不同点在于,集合是无序的,也就是说,序列要求每个位置一一相等,而集合只需要对应的元素出现次数相等即可。
我们引入一个问题:
给定一个长为n n n 的序列A A A ,每次给出两个长度相等的区间[ l 1 , r 1 ] , [ l 2 , r 2 ] [l_{1},r_{1}],[l_{2},r_{2}] [ l 1 , r 1 ] , [ l 2 , r 2 ] 里面的数排序后是否完全相等,我们就可以说[ 2 , 3 , 1 , 4 , 5 ] [2,3,1,4,5] [ 2 , 3 , 1 , 4 , 5 ] 和[ 5 , 4 , 3 , 2 , 1 ] [5,4,3,2,1] [ 5 , 4 , 3 , 2 , 1 ] 是相同的。
这种情况我们很难找到一个数据结构来支持这样的查询操作,我们发现如果称两个区间相同,那么这个区间里的每一个数的出现次数和另外一个区间中这个数的出现次数相同 。
我们考虑,只需要次数相同就可以的话,那么也就是说,这哈希值之和我数值具体是多少,而和位置无关,那么两个区间相等的必要条件就是h ( l 1 , r 1 ) = h ( l 2 , r 2 ) h(l_{1},r_{1})=h(l_{2},r_{2}) h ( l 1 , r 1 ) = h ( l 2 , r 2 ) ,现在问题在于如何设计函数使得冲突尽量小。
此时哈希函数以如下表示:
h ( { A } ) = ∑ i = 1 n h ′ ( a x ) h(\left\{ A \right\})=\sum\limits_{i=1}^n h'(a_{x}) h ( { A } ) = i = 1 ∑ n h ′ ( a x )
那么我们有方法,就是给每一个元素赋值一个随机数r x r_{x} r x ,让后h ( { A } ) = ∑ i = 1 n r a x h(\left\{ A \right\})=\sum\limits_{i=1}^n r_{a_{x}} h ( { A } ) = i = 1 ∑ n r a x 。只要随机数质量高并且值域大,冲突的概率还是比较小的。当然有的时候会被卡,考虑多做几次即可。
2. 例题给一个长度为n n n 的正整数数列a i a_{i} a i ,问有多少连续子数列,满足每个数字的出现次数均为偶数。
对于一个区间,如果每个出现次数为偶数,那么一定有⊕ i = l r a i = 0 \oplus_{i=l}^r a_{i}=0 ⊕ i = l r a i = 0 ,但是这并不代表出现次数一定为偶数,考虑给每个a i a_{i} a i 随机赋值,这样进行操作即可。
现在问题转化为当前位置i i i 有多少前缀异或和等于p r e pre p r e ,其中p r e pre p r e 表示当前前缀异或,直接扫一遍就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <bits/stdc++.h> #define int long long using namespace std;constexpr int MN=3e5 +15 ;int n,a[MN],ans,st;mt19937 mt; map<int ,int > mp,cnt; signed main () { mt.seed (time (0 )); cin>>n; st=mt (); for (int i=1 ;i<=n;i++){ cin>>a[i]; if (!mp.count (a[i])) mp[a[i]]=(st+=mt ()); a[i]=mp[a[i]]; } int pre=0 ; cnt[0 ]=1 ; for (int i=1 ;i<=n;i++){ pre^=a[i]; ans+=cnt[pre]; cnt[pre]++; } cout<<ans; return 0 ; }
给定一个n n n 个点m m m 条边的有向图,支持四种操作:
删除某条边; 删除某个点的所有入边; 恢复某条边; 恢复某个点的所有入边。 每次操作后询问当前图是否满足:
所有点出度均为 1; 所有点都处于一个环中(即基环内向树森林,每个连通块恰有一个环)。 我们考虑,这个反击图是内向基环树森林,利用性质 所有点出度均为 1 。那么既然是有向图,其实只要满足出度都为 1,就能判断。
暴力维护出度显然是O ( n m ) O(nm) O ( n m ) 。
我们发现,维护出度是很难受的,因为删入边你还需要把遍历边才能把入边全部标记上。但是维护入度很好做啊,我们不妨令原图节点u u u 的入度为g ( u ) g(u) g ( u ) ,而修改后的出度为c ( u ) c(u) c ( u ) ,初始情况下g ( u ) = c ( u ) g(u)=c(u) g ( u ) = c ( u ) ,而对于这四种操作有:
o p = 1 , c ( u ) ← c ( u ) − 1 op=1,c(u)\leftarrow c(u)-1 o p = 1 , c ( u ) ← c ( u ) − 1 。o p = 2 , c ( u ) ← 0 op=2,c(u)\leftarrow 0 o p = 2 , c ( u ) ← 0 。o p = 3 , c ( u ) ← c ( u ) + 1 op=3,c(u)\leftarrow c(u)+1 o p = 3 , c ( u ) ← c ( u ) + 1 。o p = 4 , c ( u ) ← g ( u ) op=4,c(u)\leftarrow g(u) o p = 4 , c ( u ) ← g ( u ) 。可以发现,一张图中的出度之和与入度之和是相等的,问题转化为判断和入度和和出度和为n n n ,但是这是必要条件,我们不能推出每个节点的出度均为1 1 1 ,但是我们利用和哈希就可以做到。
对于每一个节点u u u ,我们不妨给他随机赋值h ( u ) h(u) h ( u ) ,我们令g ( v ) = ∑ ( u , v ) ∈ E h ( u ) g(v)=\sum\limits_{(u,v)\in E} h(u) g ( v ) = ( u , v ) ∈ E ∑ h ( u ) ,我们可以有新的操作:
o p = 1 , c ( u ) ← c ( u ) − h ( u ) op=1,c(u)\leftarrow c(u)-h(u) o p = 1 , c ( u ) ← c ( u ) − h ( u ) 。o p = 2 , c ( u ) ← 0 op=2,c(u)\leftarrow 0 o p = 2 , c ( u ) ← 0 。o p = 3 , c ( u ) ← c ( u ) + h ( u ) op=3,c(u)\leftarrow c(u)+h(u) o p = 3 , c ( u ) ← c ( u ) + h ( u ) 。o p = 4 , c ( u ) ← g ( u ) op=4,c(u)\leftarrow g(u) o p = 4 , c ( u ) ← g ( u ) 。维护∑ c ( u ) \sum\limits c(u) ∑ c ( u ) 即可,如果有∑ c ( u ) = ∑ h ( u ) \sum\limits c(u)=\sum\limits h(u) ∑ c ( u ) = ∑ h ( u ) ,那么原图很大概率是合法图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <bits/stdc++.h> #define int long long using namespace std;constexpr int MN=5e5 +15 ;int n,m,q,hsum,now,st,h[MN],g[MN],c[MN];mt19937 mt; signed main () { mt.seed (time (0 )); cin>>n>>m; st=mt (); for (int i=1 ;i<=n;i++){ h[i]=(st+=mt ()); hsum+=h[i]; } for (int i=1 ;i<=m;i++){ int u,v; cin>>u>>v; g[v]+=h[u]; c[v]=g[v]; now+=h[u]; } cin>>q; while (q--){ int op,u,v; cin>>op; if (op==1 ){ cin>>u>>v; now-=h[u]; c[v]-=h[u]; } if (op==2 ){ cin>>u; now-=c[u]; c[u]=0 ; } if (op==3 ){ cin>>u>>v; now+=h[u]; c[v]+=h[u]; } if (op==4 ){ cin>>u; now+=g[u]-c[u]; c[u]=g[u]; } if (now==hsum){ cout<<"YES\n" ; }else cout<<"NO\n" ; } return 0 ; }
CF1746F发现直接在线做真的很不好做,考虑离线下来。注意到源题目只关心出现次数,而不关心具体取值,考虑和哈希。给每一个元素赋一个随机权值,那么每次询问我们查询s u m = ∑ i = l r h i sum=\sum\limits_{i=l}^r h_{i} s u m = i = l ∑ r h i ,若k ∣ s k|s k ∣ s 那么一定不满足,但是对于k k k 不整除s s s 这是一个必要条件,可能不成立,我们考虑多用随机值映射几次,那么成功概率就会非常高。
接下来我们只需要一个数据结构支持单调修改,区间和查询,树状数组即可,注意到答案和a i a_{i} a i 具体取值无关,考虑离散化防止爆炸:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 #include <bits/stdc++.h> #define int long long using namespace std;constexpr int MN=2e6 +15 ,MOD=1e9 +7 ;int n,q,a[MN],b[MN],c[MN],rd[MN],tot,op[MN],L[MN],R[MN],K[MN];bool ans[MN];mt19937 mt; unordered_map<int ,int > mp; namespace ly{ namespace IO { #ifndef LOCAL constexpr auto maxn=1 <<20 ; char in[maxn],out[maxn],*p1=in,*p2=in,*p3=out; #define getchar() (p1==p2&&(p2=(p1=in)+fread(in,1,maxn,stdin),p1==p2)?EOF:*p1++) #define flush() (fwrite(out,1,p3-out,stdout)) #define putchar(x) (p3==out+maxn&&(flush(),p3=out),*p3++=(x)) class Flush {public :~Flush (){flush ();}}_; #endif namespace usr { template <typename type> inline type read (type &x) { x=0 ;bool flag (0 ) ;char ch=getchar (); while (!isdigit (ch)) flag^=ch=='-' ,ch=getchar (); while (isdigit (ch)) x=(x<<1 )+(x<<3 )+(ch^48 ),ch=getchar (); return flag?x=-x:x; } template <typename type> inline void write (type x) { x<0 ?x=-x,putchar ('-' ):0 ; static short Stack[50 ],top (0 ); do Stack[++top]=x%10 ,x/=10 ;while (x); while (top) putchar (Stack[top--]|48 ); } inline char read (char &x) {do x=getchar ();while (isspace (x));return x;} inline char write (const char &x) {return putchar (x);} inline void read (char *x) {static char ch;read (ch);do *(x++)=ch;while (!isspace (ch=getchar ())&&~ch);} template <typename type>inline void write (type *x) {while (*x)putchar (*(x++));} inline void read (string &x) {static char ch;read (ch),x.clear ();do x+=ch;while (!isspace (ch=getchar ())&&~ch);} inline void write (const string &x) {for (int i=0 ,len=x.length ();i<len;++i)putchar (x[i]);} template <typename type,typename ...T>inline void read (type &x,T&...y) {read (x),read (y...);} template <typename type,typename ...T> inline void write (const type &x,const T&...y) {write (x),putchar (' ' ),write (y...),sizeof ...(y)^1 ?0 :putchar ('\n' );} template <typename type> inline void put (const type &x,bool flag=1 ) {write (x),flag?putchar ('\n' ):putchar (' ' );} } #ifndef LOCAL #undef getchar #undef flush #undef putchar #endif }using namespace IO::usr; }using namespace ly::IO::usr; struct BIT { int t[MN]; int lowbit (int x) { return x&-x; } int query (int x) { int ret=0 ; while (x){ ret+=t[x]; x-=lowbit (x); } return ret; } int query (int fl,int fr) { return query (fr)-query (fl-1 ); } void modify (int x,int k) { while (x<MN){ t[x]+=k; x+=lowbit (x); } } void clear () { memset (t,0 ,sizeof (t)); } }t; signed main () { mt.seed (time (0 )); int tim=clock (); read (n,q); for (int i=1 ;i<=n;i++){ read (a[i]); b[++tot]=a[i]; } for (int i=1 ;i<=q;i++){ read (op[i],L[i],R[i]); if (op[i]==1 ){ b[++tot]=R[i]; } else { read (K[i]); ans[i]=(R[i]-L[i]+1 )%K[i]==0 ; } } sort (b+1 ,b+1 +tot); tot=unique (b+1 ,b+1 +tot)-b-1 ; for (int i=1 ;i<=n;i++){ a[i]=lower_bound (b+1 ,b+1 +tot,a[i])-b; } for (int i=1 ;i<=q;i++){ if (op[i]==1 ) R[i]=lower_bound (b+1 ,b+1 +tot,R[i])-b; } for (;1.0 *clock ()-tim<=2.8 *CLOCKS_PER_SEC;){ t.clear (); for (int i=0 ;i<=tot;i++){ mp[i]=mt ()%MOD; } for (int i=1 ;i<=n;i++) c[i]=mp[a[i]]; for (int i=1 ;i<=n;i++) t.modify (i,c[i]); for (int i=1 ;i<=q;i++){ if (op[i]==1 ){ t.modify (L[i],mp[R[i]]-c[L[i]]); c[L[i]]=mp[R[i]]; }else if (t.query (L[i],R[i])%K[i]) ans[i]=0 ; } } for (int i=1 ;i<=q;i++){ if (op[i]!=1 ){ put ((ans[i]?"YES" :"NO" )); } } return 0 ; }
P3792 由乃与大母神原型和偶像崇拜重排为值域上连续的一段其实就是数值相邻,那么实际上数值相邻我们不关心数值具体是什么,我们只需要考虑它们能不能相邻就可以了,考虑随机赋权哈希,那么在值域连续的情况下。假设我们查询的区间随机数为p 2 , p 3 , p 4 p_2,p_3,p_4 p 2 , p 3 , p 4 ,那么通过前缀异或和算出p 2 ⊕ p 3 ⊕ p 4 p_{2}\oplus p_{3}\oplus p_{4} p 2 ⊕ p 3 ⊕ p 4 ,若区间三个随机数异或和等于p 2 ⊕ p 3 ⊕ p 4 p_{2}\oplus p_{3}\oplus p_{4} p 2 ⊕ p 3 ⊕ p 4 ,我们就认为这个区间是连续的。前缀异或和和映射前缀异或和可以用树状数组维护。
首先要离散化,但是不能瞎离散化,因为离散化后的数是连续的,我们可以考虑将离散化的时候多把每个值加 1 的数放到离散化数组里,这样本来就不连续的数离散化之后也不连续。
让后映射的数最好用 unsigned long long
自然溢出,这样的情况下极其难被 hack。若被卡随机数种子,考虑利用 random_device
这个玩意,在 Linux 下实现是一个真随机数生成器,从系统的熵池获取数据,通常用于为其他随机数引擎提供种子。这样能被 hack 概率可以忽略。
推荐使用茅台 19937 产随机数和 time(0)
初始化种子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 #include <bits/stdc++.h> #define int long long #define ull unsigned long long using namespace std;constexpr int MN=3e6 +15 ;int n,m,tot,a[MN],b[MN],op[MN],x[MN],y[MN];ull rd[MN],pre[MN]; mt19937 mt; struct BITsum { int t[MN]; int lowbit (int x) {return x&-x;} int query (int x) { int ret=0 ; while (x){ ret+=t[x]; x-=lowbit (x); } return ret; } void modify (int x,int k) { while (x<MN){ t[x]+=k; x+=lowbit (x); } } }t1; struct BITXor { ull t[MN]; int lowbit (int x) { return x&-x; } ull query (int x) { int ret=0 ; while (x){ ret^=t[x]; x-=lowbit (x); } return ret; } void modify (int x,ull k) { while (x<MN){ t[x]^=k; x+=lowbit (x); } } }t2; signed main () { mt.seed (time (0 )); cin>>n>>m; for (int i=1 ;i<=n;i++){ cin>>a[i]; b[++tot]=a[i]; b[++tot]=a[i]+1 ; } for (int i=1 ;i<=m;i++){ cin>>op[i]>>x[i]>>y[i]; if (op[i]==1 ) b[++tot]=y[i]+1 ,b[++tot]=y[i]; } sort (b+1 ,b+tot+1 ); tot=unique (b+1 ,b+1 +tot)-b-1 ; rd[0 ]=mt (); for (ull i=1 ,st=rd[0 ];i<=tot;i++){ rd[i]=(st+=mt ()); pre[i]=pre[i-1 ]^rd[i]; } for (int i=1 ;i<=n;i++){ a[i]=lower_bound (b+1 ,b+1 +tot,a[i])-b; t1. modify (i,a[i]); t2. modify (i,rd[a[i]]); } for (int i=1 ;i<=m;i++){ if (op[i]==1 ){ y[i]=lower_bound (b+1 ,b+1 +tot,y[i])-b; t1. modify (x[i],y[i]-a[x[i]]); t2. modify (x[i],rd[y[i]]^rd[a[x[i]]]); a[x[i]]=y[i]; }else { int mid=(t1. query (y[i])-t1. query (x[i]-1 ))/(y[i]-x[i]+1 ); int l,r; l=mid-(y[i]-x[i])/2 ; if ((y[i]-x[i])&1 ) r=mid+(y[i]-x[i])/2 +1 ; else r=mid+(y[i]-x[i])/2 ; if (l<=0 ||r>=tot) cout<<"yuanxing\n" ; else if ((t2. query (y[i])^t2. query (x[i]-1 ))==(pre[r]^pre[l-1 ])){ cout<<"damushen\n" ; } else cout<<"yuanxing\n" ; } } return 0 ; }
3. 后言属于是乱搞操作,但是又没有那么乱搞哈哈哈,学到也是赚到 lol。