分块
分块1
给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。
这是一道能用许多数据结构优化的经典题,可以用于不同数据结构训练。
数列分块就是把数列中每m个元素打包起来,达到优化算法的目的。
以此题为例,如果我们把每m个元素分为一块,共有n/m块,每次区间加的操作会涉及O(n/m)个整块,以及区间两侧两个不完整的块中至多2m个元素。
我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块直接O(1)标记,而不完整的块由于元素比较少,暴力修改元素的值。
每次询问时返回元素的值加上其所在块的加法标记。
这样每次操作的复杂度是O(n/m)+O(m),根据均值不等式,当m取√n时总复杂度最低,为了方便,我们都默认下文的分块大小为√n。
#include<map> #include<set> #include<cmath> #include<stack> #include<queue> #include<cstdio> #include<vector> #include<cstring> #include<cstdlib> #include<iostream> #include<algorithm> #define mod 998244353 #define pi acos(-1) #define inf 0x7fffffff #define ll long long using namespace std; ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,blo; int v[50005],bl[50005],atag[50005]; void add(int a,int b,int c) { for(int i=a;i<=min(bl[a]*blo,b);i++) v[i]+=c; if(bl[a]!=bl[b]) for(int i=(bl[b]-1)*blo+1;i<=b;i++) v[i]+=c; for(int i=bl[a]+1;i<=bl[b]-1;i++) atag[i]+=c; } int main() { n=read();blo=sqrt(n); for(int i=1;i<=n;i++)v[i]=read(); for(int i=1;i<=n;i++)bl[i]=(i-1)/blo+1; for(int i=1;i<=n;i++) { int f=read(),a=read(),b=read(),c=read(); if(f==0)add(a,b,c); if(f==1)printf("%d\n",v[b]+atag[bl[b]]); } return 0; }
分块 2
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的元素个数。
有了上一题的经验,我们可以发现,数列简单分块问题实际上有三项东西要我们思考:
对于每次区间操作:
1.不完整的块 的O(√n)个元素怎么处理?
2.O(√n)个 整块 怎么处理?
3.要预处理什么信息(复杂度不能超过后面的操作)?
我们先来思考只有询问操作的情况,不完整的块枚举统计即可;而要在每个整块内寻找小于一个值的元素数,于是我们不得不要求块内元素是有序的,这样就能使用二分法对块内查询,需要预处理时每块做一遍排序,复杂度O(nlogn),每次查询在√n个块内二分,以及暴力2√n个元素,总复杂度O(nlogn + n√nlog√n)。
可以通过均值不等式计算出更优的分块大小,就不展开讨论了
那么区间加怎么办呢?
套用第一题的方法,维护一个加法标记,略有区别的地方在于,不完整的块修改后可能会使得该块内数字乱序,所以头尾两个不完整块需要重新排序,复杂度分析略。
在加法标记下的询问操作,块外还是暴力,查询小于(x – 加法标记)的元素个数,块内用(x – 加法标记)作为二分的值即可。
#include<map> #include<set> #include<cmath> #include<stack> #include<queue> #include<cstdio> #include<vector> #include<cstring> #include<cstdlib> #include<iostream> #include<algorithm> #define mod 998244353 #define pi acos(-1) #define inf 0x7fffffff #define ll long long using namespace std; ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,blo; int v[50005],bl[50005],atag[50005]; vector<int>ve[505]; void reset(int x) { ve[x].clear(); for(int i=(x-1)*blo+1;i<=min(x*blo,n);i++) ve[x].push_back(v[i]); sort(ve[x].begin(),ve[x].end()); } void add(int a,int b,int c) { for(int i=a;i<=min(bl[a]*blo,b);i++) v[i]+=c; reset(bl[a]); if(bl[a]!=bl[b]) { for(int i=(bl[b]-1)*blo+1;i<=b;i++) v[i]+=c; reset(bl[b]); } for(int i=bl[a]+1;i<=bl[b]-1;i++) atag[i]+=c; } int query(int a,int b,int c) { int ans=0; for(int i=a;i<=min(bl[a]*blo,b);i++) if(v[i]+atag[bl[a]]<c)ans++; if(bl[a]!=bl[b]) for(int i=(bl[b]-1)*blo+1;i<=b;i++) if(v[i]+atag[bl[b]]<c)ans++; for(int i=bl[a]+1;i<=bl[b]-1;i++) { int x=c-atag[i]; ans+=lower_bound(ve[i].begin(),ve[i].end(),x)-ve[i].begin(); } return ans; } int main() { n=read();blo=sqrt(n); for(int i=1;i<=n;i++)v[i]=read(); for(int i=1;i<=n;i++) { bl[i]=(i-1)/blo+1; ve[bl[i]].push_back(v[i]); } for(int i=1;i<=bl[n];i++) sort(ve[i].begin(),ve[i].end()); for(int i=1;i<=n;i++) { int f=read(),a=read(),b=read(),c=read(); if(f==0)add(a,b,c); if(f==1)printf("%d\n",query(a,b,c*c)); } return 0; }
分块3
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。
n<=100000其实是为了区分暴力和一些常数较大的写法。
接着第二题的解法,其实只要把块内查询的二分稍作修改即可。
不过这题其实想表达:可以在块内维护其它结构使其更具有拓展性,比如放一个 set ,这样如果还有插入、删除元素的操作,会更加的方便。
分块的调试检测技巧:
可以生成一些大数据,然后用两份分块大小不同的代码来对拍,还可以根据运行时间尝试调整分块大小,减小常数。
#include<map> #include<set> #include<cmath> #include<stack> #include<queue> #include<cstdio> #include<vector> #include<cstring> #include<cstdlib> #include<iostream> #include<algorithm> #define mod 998244353 #define pi acos(-1) #define inf 0x7fffffff #define ll long long using namespace std; ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,blo; int v[100005],bl[100005],atag[100005]; set<int>st[105]; void add(int a,int b,int c) { for(int i=a;i<=min(bl[a]*blo,b);i++) { st[bl[a]].erase(v[i]); v[i]+=c; st[bl[a]].insert(v[i]); } if(bl[a]!=bl[b]) { for(int i=(bl[b]-1)*blo+1;i<=b;i++) { st[bl[b]].erase(v[i]); v[i]+=c; st[bl[b]].insert(v[i]); } } for(int i=bl[a]+1;i<=bl[b]-1;i++) atag[i]+=c; } int query(int a,int b,int c) { int ans=-1; for(int i=a;i<=min(bl[a]*blo,b);i++) { int val=v[i]+atag[bl[a]]; if(val<c)ans=max(val,ans); } if(bl[a]!=bl[b]) for(int i=(bl[b]-1)*blo+1;i<=b;i++) { int val=v[i]+atag[bl[b]]; if(val<c)ans=max(val,ans); } for(int i=bl[a]+1;i<=bl[b]-1;i++) { int x=c-atag[i]; set<int>::iterator it=st[i].lower_bound(x); if(it==st[i].begin())continue; --it; ans=max(ans,*it+atag[i]); } return ans; } int main() { n=read();blo=1000; for(int i=1;i<=n;i++)v[i]=read(); for(int i=1;i<=n;i++) { bl[i]=(i-1)/blo+1; st[bl[i]].insert(v[i]); } for(int i=1;i<=n;i++) { int f=read(),a=read(),b=read(),c=read(); if(f==0)add(a,b,c); if(f==1)printf("%d\n",query(a,b,c)); } return 0; }
分块 4
给出一个长为n的数列,以及n个操作,操作涉及区间加法,区间求和。
这题的询问变成了区间上的询问,不完整的块还是暴力;而要想快速统计完整块的答案,需要维护每个块的元素和,先要预处理一下。
考虑区间修改操作,不完整的块直接改,顺便更新块的元素和;完整的块类似之前标记的做法,直接根据块的元素和所加的值计算元素和的增量。
#include<map> #include<set> #include<cmath> #include<stack> #include<queue> #include<cstdio> #include<vector> #include<cstring> #include<cstdlib> #include<iostream> #include<algorithm> #define mod 998244353 #define pi acos(-1) #define inf 0x7fffffff #define ll long long using namespace std; ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,blo; int bl[50005]; ll v[50005],atag[50005],sum[50005]; void add(int a,int b,int c) { for(int i=a;i<=min(bl[a]*blo,b);i++) v[i]+=c,sum[bl[a]]+=c;; if(bl[a]!=bl[b]) for(int i=(bl[b]-1)*blo+1;i<=b;i++) v[i]+=c,sum[bl[b]]+=c; for(int i=bl[a]+1;i<=bl[b]-1;i++) atag[i]+=c; } ll query(int a,int b) { ll ans=0; for(int i=a;i<=min(bl[a]*blo,b);i++) ans+=v[i]+atag[bl[a]]; if(bl[a]!=bl[b]) for(int i=(bl[b]-1)*blo+1;i<=b;i++) ans+=v[i]+atag[bl[b]]; for(int i=bl[a]+1;i<=bl[b]-1;i++) ans+=sum[i]+blo*atag[i]; return ans; } int main() { n=read();blo=sqrt(n); for(int i=1;i<=n;i++)v[i]=read(); for(int i=1;i<=n;i++) { bl[i]=(i-1)/blo+1; sum[bl[i]]+=v[i]; } for(int i=1;i<=n;i++) { int f=read(),a=read(),b=read(),c=read(); if(f==0)add(a,b,c); if(f==1) printf("%d\n",query(a,b)%(c+1)); } return 0; }
分块5
给出一个长为n的数列,以及n个操作,操作涉及区间开方,区间求和。
稍作思考可以发现,开方操作比较棘手,主要是对于整块开方时,必须要知道每一个元素,才能知道他们开方后的和,也就是说,难以快速对一个块信息进行更新。
看来我们要另辟蹊径。不难发现,这题的修改就只有下取整开方,而一个数经过几次开方之后,它的值就会变成 0 或者 1。
如果每次区间开方只不涉及完整的块,意味着不超过2√n个元素,直接暴力即可。
如果涉及了一些完整的块,这些块经过几次操作以后就会都变成 0 / 1,于是我们采取一种分块优化的暴力做法,只要每个整块暴力开方后,记录一下元素是否都变成了 0 / 1,区间修改时跳过那些全为 0 / 1 的块即可。
这样每个元素至多被开方不超过4次,显然复杂度没有问题。
#include<bits/stdc++.h> #define ll long long using namespace std; const int maxn = 50086; int n, a[maxn]; int sum[maxn], pos[maxn]; int L[maxn], R[maxn]; int t; bool vis[maxn]; int opt, l, r, c; inline int read() { int x = 0, y = 1; char ch = getchar(); while(!isdigit(ch)) { if(ch == '-') y = -1; ch = getchar(); } while(isdigit(ch)) { x = (x << 1) + (x << 3) + ch - '0'; ch = getchar(); } return x * y; } inline void check_sqrt(int x) { if(vis[x]) return; vis[x] = 1; sum[x] = 0; for(int i = L[x]; i <= R[x]; ++i) { a[i] = sqrt(a[i]); sum[x] += a[i]; if(a[i] > 1) vis[x] = 0; } } inline void change(int l, int r) { int p = pos[l], q = pos[r]; if(p == q) { for(int i = l; i <= r; ++i) { sum[p] -= a[i]; a[i] = sqrt(a[i]); sum[p] += a[i]; } } else { for(int i = p + 1; i <= q - 1; ++i) check_sqrt(i); for(int i = l; i <= R[p]; ++i) { sum[p] -= a[i]; a[i] = sqrt(a[i]); sum[p] += a[i]; } for(int i = L[q]; i <= r; ++i) { sum[q] -= a[i]; a[i] = sqrt(a[i]); sum[q] += a[i]; } } } inline int ask(int l, int r) { int ans = 0; int p = pos[l], q = pos[r]; if(p == q) for(int i = l; i <= r; ++i) ans += a[i]; else { for(int i = p + 1; i <= q - 1; ++i) ans += sum[i]; for(int i = l; i <= R[p]; ++i) ans += a[i]; for(int i = L[q]; i <= r; ++i) ans += a[i]; } return ans; } int main() { memset(vis, false, sizeof(vis)); n = read(); t = sqrt(n); for(int i = 1; i <= n; ++i) a[i] = read(); for(int i = 1; i <= t; ++i) { L[i] = (i - 1) * t + 1; R[i] = i * t; } if(R[t] < n) t++, L[t] = R[t - 1] + 1, R[t] = n; for(int i = 1; i <= t; ++i) for(int j = L[i]; j <= R[i]; ++j) { sum[i] += a[j]; pos[j] = i; } for(int i = 1; i <= n; ++i) { opt = read(), l = read(), r = read(), c = read(); if(!opt) change(l, r); else cout << ask(l, r) << '\n'; } return 0; }
分块6
给出一个长为n的数列,以及n个操作,操作涉及单点插入,单点询问,数据随机生成。
先说随机数据的情况
之前提到过,如果我们块内用数组以外的数据结构,能够支持其它不一样的操作,比如此题每块内可以放一个动态的数组,每次插入时先找到位置所在的块,再暴力插入,把块内的其它元素直接向后移动一位,当然用链表也是可以的。
查询的时候类似,复杂度分析略。
但是这样做有个问题,如果数据不随机怎么办?
如果先在一个块有大量单点插入,这个块的大小会大大超过√n,那块内的暴力就没有复杂度保证了。
还需要引入一个操作:重新分块(重构)
每根号n次插入后,重新把数列平均分一下块,重构需要的复杂度为O(n),重构的次数为√n,所以重构的复杂度没有问题,而且保证了每个块的大小相对均衡。
当然,也可以当某个块过大时重构,或者只把这个块分成两半。
#include<map> #include<set> #include<cmath> #include<stack> #include<queue> #include<cstdio> #include<vector> #include<cstring> #include<cstdlib> #include<iostream> #include<algorithm> #define mod 998244353 #define pi acos(-1) #define inf 0x7fffffff #define ll long long using namespace std; ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,blo,m; int v[100005]; vector<int>ve[1005]; int st[200005],top; pair<int,int> query(int b) { int x=1; while(b>ve[x].size()) b-=ve[x].size(),x++; return make_pair(x,b-1); } void rebuild() { top=0; for(int i=1;i<=m;i++) { for(vector<int>::iterator j=ve[i].begin();j!=ve[i].end();j++) st[++top]=*j; ve[i].clear(); } int blo2=sqrt(top); for(int i=1;i<=top;i++) ve[(i-1)/blo2+1].push_back(st[i]); m=(top-1)/blo2+1; } void insert(int a,int b) { pair<int,int> t=query(a); ve[t.first].insert(ve[t.first].begin()+t.second,b); if(ve[t.first].size()>20*blo) rebuild(); } int main() { n=read();blo=sqrt(n); for(int i=1;i<=n;i++)v[i]=read(); for(int i=1;i<=n;i++) ve[(i-1)/blo+1].push_back(v[i]); m=(n-1)/blo+1; for(int i=1;i<=n;i++) { int f=read(),a=read(),b=read(),c=read(); if(f==0)insert(a,b); if(f==1) { pair<int,int> t=query(b); printf("%d\n",ve[t.first][t.second]); } } return 0; }
分块7
给出一个长为n的数列,以及n个操作,操作涉及区间乘法,区间加法,单点询问。
很显然,如果只有区间乘法,和分块入门 1 的做法没有本质区别,但要思考如何同时维护两种标记。
我们让乘法标记的优先级高于加法(如果反过来的话,新的加法标记无法处理)
若当前的一个块乘以m1后加上a1,这时进行一个乘m2的操作,则原来的标记变成m1*m2,a1*m2
若当前的一个块乘以m1后加上a1,这时进行一个加a2的操作,则原来的标记变成m1,a1+a2
#include<map> #include<set> #include<cmath> #include<stack> #include<queue> #include<cstdio> #include<vector> #include<cstring> #include<cstdlib> #include<iostream> #include<algorithm> #define mod 10007 #define pi acos(-1) #define inf 0x7fffffff #define ll long long using namespace std; ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,blo; int v[100005],bl[100005],atag[1005],mtag[1005]; void reset(int x) { for(int i=(x-1)*blo+1;i<=min(n,x*blo);i++) v[i]=(v[i]*mtag[x]+atag[x])%mod; atag[x]=0;mtag[x]=1; } void solve(int f,int a,int b,int c) { reset(bl[a]); for(int i=a;i<=min(bl[a]*blo,b);i++) { if(f==0)v[i]+=c; else v[i]*=c; v[i]%=mod; } if(bl[a]!=bl[b]) { reset(bl[b]); for(int i=(bl[b]-1)*blo+1;i<=b;i++) { if(f==0)v[i]+=c; else v[i]*=c; v[i]%=mod; } } for(int i=bl[a]+1;i<=bl[b]-1;i++) { if(f==0)atag[i]=(atag[i]+c)%mod; else { atag[i]=(atag[i]*c)%mod; mtag[i]=(mtag[i]*c)%mod; } } } int main() { n=read();blo=sqrt(n); for(int i=1;i<=n;i++)v[i]=read(); for(int i=1;i<=n;i++)bl[i]=(i-1)/blo+1; for(int i=1;i<=bl[n];i++)mtag[i]=1; for(int i=1;i<=n;i++) { int f=read(),a=read(),b=read(),c=read(); if(f==2)printf("%d\n",(v[b]*mtag[bl[b]]+atag[bl[b]])%mod); else solve(f,a,b,c); } return 0; }
分块8
给出一个长为n的数列,以及n个操作,操作涉及区间询问等于一个数c的元素,并将这个区间的所有元素改为c。
区间修改没有什么难度,这题难在区间查询比较奇怪,因为权值种类比较多,似乎没有什么好的维护方法。
模拟一些数据可以发现,询问后一整段都会被修改,几次询问后数列可能只剩下几段不同的区间了。
我们思考这样一个暴力,还是分块,维护每个分块是否只有一种权值,区间操作的时候,对于同权值的一个块就O(1)统计答案,否则暴力统计答案,并修改标记,不完整的块也暴力。
这样看似最差情况每次都会耗费O(n)的时间,但其实可以这样分析:
假设初始序列都是同一个值,那么查询是O(√n),如果这时进行一个区间操作,它最多破坏首尾2个块的标记,所以只能使后面的询问至多多2个块的暴力时间,所以均摊每次操作复杂度还是O(√n)。
换句话说,要想让一个操作耗费O(n)的时间,要先花费√n个操作对数列进行修改。
初始序列不同值,经过类似分析后,就可以放心的暴力啦。
#include<map> #include<set> #include<cmath> #include<stack> #include<queue> #include<cstdio> #include<vector> #include<cstring> #include<cstdlib> #include<iostream> #include<algorithm> #define mod 998244353 #define pi acos(-1) #define inf 0x7fffffff #define ll long long using namespace std; ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,blo; int v[100005],bl[100005],tag[100005]; void reset(int x) { if(tag[x]==-1)return; for(int i=(x-1)*blo+1;i<=blo*x;i++) v[i]=tag[x]; tag[x]=-1; } int solve(int a,int b,int c) { int ans=0; reset(bl[a]); for(int i=a;i<=min(bl[a]*blo,b);i++) if(v[i]!=c)v[i]=c; else ans++; if(bl[a]!=bl[b]) { reset(bl[b]); for(int i=(bl[b]-1)*blo+1;i<=b;i++) if(v[i]!=c)v[i]=c; else ans++; } for(int i=bl[a]+1;i<=bl[b]-1;i++) if(tag[i]!=-1) { if(tag[i]!=c)tag[i]=c; else ans+=blo; } else { for(int j=(i-1)*blo+1;j<=i*blo;j++) if(v[j]!=c)v[j]=c; else ans++; tag[i]=c; } return ans; } int main() { memset(tag,-1,sizeof(tag)); n=read();blo=sqrt(n); for(int i=1;i<=n;i++)v[i]=read(); for(int i=1;i<=n;i++)bl[i]=(i-1)/blo+1; for(int i=1;i<=n;i++) { int a=read(),b=read(),c=read(); printf("%d\n",solve(a,b,c)); } return 0; }
分块9
给出一个长为n的数列,以及n个操作,操作涉及询问区间的最小众数。
给出一个长为 n 的数列,以及 n 个操作,操作涉及询问区间的最小众数。
分块最经典的区间众数操作,因为不具有区间相加性,无法使用线段树或是树状数组完成。
当然学了莫队就很简单,不会莫队(比如我),就老老实实分块吧....
首先先离散化处理一下
接着枚举预处理出任意一个区间[l, r]的众数,也就是任意一个块的左端点到其他块的右端点的众数(看了代码就懂了系列),非常显然的使用数组记录每个数出现过的次数,然后再枚举一遍,求出众数
对于每一次询问的区间[l, r],我们可以和明显的发现,大区间的众数存在于大区间内完整的块里或是左右两端不完整的块里
对于完整的块里,显然我们已经与处理过了,直接询问即可,所以我们需要快速求出不完整的块中的区间众数
然后可以发现我们上面进行了离散化,所以我们在离散时动动手脚就很好办了,对于每数,使用vector存每一次出现的位置,
然后把这个数存在的左端点和右端点二分再相减就可以求出这个数在区间里出现的次数,也就是求区间内第一个大于这个数的位置(upper_bound)和第一个大于等于这个数的位置(lower_bound)
再相减即可
剩下的直接看代码即可
#include<bits/stdc++.h> using namespace std; const int maxn = 50005; int f[505][505]; int a[maxn], pos[maxn]; int val[maxn], cnt[maxn]; int n, l, r, t, id; map<int, int> mp; vector<int> v[maxn]; inline int read() { int x = 0, y = 1; char ch = getchar(); while(!isdigit(ch)) { if(ch == '-') y = -1; ch = getchar(); } while(isdigit(ch)) { x = (x << 1) + (x << 3) + ch - '0'; ch = getchar(); } return x * y; } inline void pre(int x) { memset(cnt, 0, sizeof(cnt)); int maxx = 0 , ans = 0; for(int i = (x - 1) * t + 1; i <= n; ++i) { cnt[a[i]]++; int p = pos[i]; if(cnt[a[i]] > maxx || ((cnt[a[i]] == maxx) && val[a[i]] < val[ans])) ans = a[i], maxx = cnt[a[i]]; f[x][p] = ans; } } inline int query_cnt(int l, int r, int x) { int k = upper_bound(v[x].begin(), v[x].end(), r) - lower_bound(v[x].begin(), v[x].end(), l); return k; } inline int query(int l, int r) { int ans, maxx = -1000000; ans = f[pos[l] + 1][pos[r] - 1]; maxx = query_cnt(l, r, ans); for(int i = l; i <= min(pos[l] * t, r); ++i) { int k = query_cnt(l, r, a[i]); if(k > maxx || (k == maxx && val[ans] > val[a[i]])) ans = a[i], maxx = k; } if(pos[l] != pos[r]) { for(int i = (pos[r] - 1) * t + 1; i <= r; ++i) { int k = query_cnt(l, r, a[i]); if(k > maxx || (k == maxx && val[ans] > val[a[i]])) ans = a[i], maxx = k; } } return ans; } int main() { n = read(); t = 200; for(int i = 1; i <= n; ++i) { a[i] = read(); if(!mp[a[i]]) { mp[a[i]] = ++id; val[id] = a[i]; } a[i] = mp[a[i]]; v[a[i]].push_back(i); } for(int i = 1; i <= n; ++i) pos[i] = (i - 1) / t + 1; for(int i = 1; i <= pos[n]; ++i) pre(i); for(int i = 1; i <= n; ++i) { l = read(), r = read(); if(l > r) swap(l, r); cout << val[query(l, r)] << '\n'; } return 0; }
毒瘤
题面:给定一个长度为n的序列,求有多少个区间[l, r]满足:区间内每个数都出现奇数次
解决的主要思想是:
给每个数x赋上一个随机权值Hx,这样就能把问题转化成:
求有多少个区间,使得区间内的所有数权值异或和等于区间内出现过的数权值异或和
思考为什么会有这样的转化:
首先我们可以知道,对于有着相同权值的数,若出现了多次肯定会不断抵消。
当一个数x出现了奇数次时,将区间内所有的x异或起来,得到的仍是x,这样也就是说,若区间内的所有数异或起来等于区间内出现过的
数的权值异或和
就说明这个区间内每个数都出现了奇数次
接下来思考基于这个思想的具体算法:
记prei表示i这个数上一次出现的位置。
那么问题转化为:枚举区间右端点𝑟,每次两个操作:
1. 给区间 [1, prear]异或一个数;
2. 询问[1, r]中为0的数的个数
为什么可以这样呢,注意我们的prei的含义:它表示的是这个数上次出现的位置
我们考虑我们将要插入一个数x,若[1, prear]之间异或上这个x的权值Hx,然后里面部分数变成了0,说明了什么:
说明了在区间[1, prear]中,x这个数出现了奇数次,这样我们枚举右端点r,然后每次这样操作,再查询0的个数,用一个变量ans累加答案
最后得到的就是有多少个区间满足每个数出现了奇数次了
然后考虑代码实现:
因为数据范围可能较大,所以我们素质离散一下
同时为了避免重复,我们rand出的权值可能会很大,所以我们要注意使用unsigned long long
对于区间异或我们可以打标记,区间询问即为统计值为标记的数的个数。
同时关于哪个数赋了什么权值,我们也需要存储,比如说我们可以用STL的map来实现
但是众所周知map有O(logn)的复杂度,而且即便我们如此取权值也可能会有重复
因此我们可以选择毒瘤hash
遇到不完整的块的时候,直接暴力处理过后把块的信息重新统计即可
这样复杂度就是O(n√n)
但是口胡算法并没什么用处,代码实现还是相当的毒瘤的,某个写std的人如是说道:“哎我当初在干什么”“哎我都写了什么”
所以具体细节还是代码见吧
#include<bits/stdc++.h> #define ll long long #define uint unsigned int #define ull unsigned long long using namespace std; const int maxn = 2e5 + 10; const int kuai = 510; const int MOD = 2339;//uss it to hash struct shiki { int lin[MOD + 5], net[MOD + 5], len; int hid[MOD + 5], cnt[MOD + 5], id; ull to[MOD + 5]; inline void clear_() { len = id = 0; memset(lin, 0, sizeof(lin)); memset(hid, 0, sizeof(hid)); } inline void insert(int xx, ull yy, int v) { to[++len] = yy; net[len] = lin[xx]; cnt[len] = v; lin[xx] = len; } //最初没有插入任何数,对每个单独的数组成的只有一个区间的数来说,出现次数都是奇数次,因此赋上(r-l+1)的初值 //cnt[i]表示对于某个数出现了多少次,下标i对应在链中的位置(大概),参考邻接表 inline void init(int L, int R) {//最初 insert(0, 0, R - L + 1); } inline void reset() {//reset:重置, 在将块重构时,因为直接memset的复杂度可能是错误的,所以每次把tot归零 id++, len = 0;//同时用一个id表示最新版本是哪个版本以方便把后续节点安排上 } inline int place(int x) {//hid数组表示对于一个取模后的数hx,它是哪一个版本的,方便把它更新掉 if(hid[x] == id) return lin[x];//若当前值在新的表中出现过,return head[x] else { hid[x] = id;//else 把当前值插入新的hash表中 return lin[x] = 0; } } inline void add(ull x) { int hx = x % MOD; for(int i = place(hx); i; i = net[i]) if(to[i] == x) {//表示在这个新的hash块里是否存在和取模后的x值相等的,若相等,则说明插入后to[i]的数量++ cnt[i]++;//则这个数的数量++ return; } insert(hx, x, 1);//若未找到,将x插入 } inline int query_cnt(ull x) {//询问针对完整的块tag表示这些块异或了数次后的值 int hx = x % MOD; for(int i = place(hx); i; i = net[i])//搜索hash值为hx的数,若此前有和这个值相等的数,则说明异或上x后,可以得到0,也就是说在x前,这个数出现了奇数次 if(to[i] == x) return cnt[i];//则符合要求,输出这个数的个数 return 0; } }hash[kuai]; int a[maxn]; ull H[maxn];//H数组表示赋上的随机权值 int pos[maxn], L[maxn], R[maxn]; int b[maxn], c[maxn], tot = 0; ull tag[kuai];//对于整个的块表示异或了数次后的值 ull cag[maxn];//对于非整块的表示异或了数次后的值 int n, m, t; int last[maxn];//数a[i]上一次出现的位置 inline int read() { int x = 0, y = 1; char ch = getchar(); while(!isdigit(ch)) { if(ch == '-') y = -1; ch = getchar(); } while(isdigit(ch)) { x = (x << 1) + (x << 3) + ch - '0'; ch = getchar(); } return x * y; } inline int query_lisan(int x) { return lower_bound(b + 1, b + tot + 1, x) - b; } inline ull irand() { ull res = 0; for(int i = 1; i <= 5; ++i) res = (((res + rand()) * rand()) << 15) + rand(); return res; } inline void deal_xor(int x, ull hx) { if(!x) return; int p = pos[x]; for(int i = 1; i < p; ++i) tag[i] ^= hx; hash[p].reset(); for(int i = L[p]; i <= x; ++i) { cag[i] ^= hx; hash[p].add(cag[i]); } for(int i = x + 1; i <= R[p]; ++i) hash[p].add(cag[i]); } inline int query(int x) { int p = pos[x], res = 0; for(int i = 1; i < p; ++i) res += hash[i].query_cnt(tag[i]); for(int i = L[p]; i <= x; ++i) if(cag[i] == tag[p]) res++; return res; } int main() { n = read(); t = sqrt(n); for(int i = 1; i <= n; ++i) { a[i] = read(); c[i] = a[i]; } //离散化 sort(c + 1, c + n + 1); for(int i = 1; i <= n; ++i) if(i == 1 || c[i - 1] != c[i]) b[++tot] = c[i]; for(int i = 1; i <= n; ++i) { int pal = query_lisan(a[i]); c[i] = pal;//c数组为离散后的a数组 } //check // for(int i = 1; i <= n; ++i) cout << c[i] << ' '; // cout << '\n'; //分块 for(int i = 1; i <= t; ++i) { L[i] = (i - 1) * t + 1; R[i] = i * t; } if(R[t] < n) t++, L[t] = R[t - 1] + 1, R[t] = n; for(int i = 1; i <= t; ++i) for(int j = L[i]; j <= R[i]; ++j) pos[j] = i; ll ans = 0; //start to answer questions for(int i = 1; i <= tot; ++i) H[i] = irand();//赋一个随机权值 for(int i = 1; i <= t; ++i) hash[i].clear_(), hash[i].init(L[i], R[i]); for(int i = 1; i <= n; ++i) { int pre = last[c[i]]; last[c[i]] = i; deal_xor(pre, H[c[i]]); ans += query(i); } printf("%lld\n", ans); return 0; }