题解 P5906 【【模板】回滚莫队】
yuzhechuan · · 题解
感觉并不用特意去卡常啊,只要不滥用memset就能轻松地跑很快了
还有话说虽然这是道模板题,但我觉得AT1219 歴史の研究可能才是更多人的回滚莫队模板题
题解:
传统莫队众所周知有两个操作:增和减
增的操作一般情况下都是好做的,但减就不一定了
例如本题,想要删最左边界的话,就要知道次左边界,然后又依次类推,这就不是“优雅的暴力”了
而回滚莫队就是解决这种问题的利器,大致思想是利用更多的增操作代替减操作
也就是我们现在要构造出一种方案,复杂度有保障的情况下,使得l指针和r指针都能(尽量)一直往自己的正方向走
r指针很好解决,排序时将询问的r边界按从小到大排就好了
l指针呢,运用分块的思想我们可以将l边界在同一块中的询问排序排在一起以便一起处理,具体的处理方法就是不断地将l指针拉回块的右边界,拉到询问的位置,拉回块的右边界,拉到询问的位置。。。
稍微总结一下:分块莫队就是将左边界在同一块中的询问放在一起处理,r指针可以通过排序实现单调移动,而l指针可以不断地拉回当前块的右边,再对于每个询问单调地向左移动(脑补反复横跳),由于块的大小是根号的,因此对于一次询问,l指针的移动始终不会超过根号次。另外有些询问可能左右边界是在同一块中的,这时我们就可以暴力遍历一遍,反正长度不会超过根号嘛
让我们来看下这道题的具体情况:
我们记左指针为
然后答案会有三种情况出现:
-
答案完全在左区间中
-
答案完全在右区间中
-
答案一部分在左区间中,一部分在右区间中
注意到答案的右端点是单调的,因此取个max即可,与当前扫到的下标减一减就解决了情况1和3
对于情况2,我们要额外记录一个值,记录一个数在右区间中第一次出现的位置
假如max也在右区间中,减一减,就得到了情况2的答案
然后简单讲一下清空桶的方法
用暴力的memset肯定是不优秀的,因为有许多冗余操作(他都没赋过值,你为啥要清空?),为了避免这些冗余,我们可以开个数组记录下我们曾经对哪些桶做过修改,最后在遍历下这个数组将那些曾经被修改过的桶赋回0就好了,而记录数组的清空只要
代码:
#include <bits/stdc++.h>
using namespace std;
template<class t> inline t read(t &x){
char c=getchar();bool f=0;x=0;
while(!isdigit(c)) f|=c=='-',c=getchar();
while(isdigit(c)) x=(x<<1)+(x<<3)+(c^48),c=getchar();
if(f) x=-x;return x;
}
template<class t> inline void write(t x){
if(x<0) putchar('-'),write(-x);
else{if(x>9) write(x/10);putchar('0'+x%10);}
}
const int N=2e5+5;
int un,n,m,a[N],ANS[N],ma[N],b[N],bn,num[N],st[N],cn,clear[N];
//变量的解释下面的代码中都有哦!!!
struct que{
int l,r,i;
inline bool operator < (const que &nt) const {
return (b[l]^b[nt.l])?b[l]<b[nt.l]:r<nt.r; //先按左边界所在块排,相同时再按右边界排
}
}q[N];
inline int max(const int &x,const int &y){
return x>y?x:y;
}
int calc(int l,int r){ //暴力扫一遍
int last[N],res=0;
for(int i=l;i<=r;i++) last[a[i]]=0; //记录每个数最早出现的位置
for(int i=l;i<=r;i++) if(!last[a[i]]) last[a[i]]=i; else res=max(res,i-last[a[i]]);
return res;
}
signed main(){
read(n);
int len=sqrt(n); //块长
for(int i=1;i<=n;i++) num[i]=read(a[i]),b[i]=(i-1)/len+1; //b记录每个下标是在哪个块中的
bn=b[n]; //块数
sort(num+1,num+1+n);
un=unique(num+1,num+1+n)-num-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(num+1,num+1+un,a[i])-num; //正常的离散操作
read(m);
for(int i=1;i<=m;i++){
read(q[i].l);read(q[i].r);
q[i].i=i;
}
sort(q+1,q+1+m); //询问排序
for(int i=1,j=1;j<=bn;j++){ //i枚举询问,j枚举询问的左边界所在块
int br=min(n,j*len),l=br+1,r=l-1,ans=0; //br是当前块的右边界
cn=0; //清空记录数组的指针
for(;b[q[i].l]==j;i++){ //枚举当前块内的询问
if(b[q[i].r]==j){ //如果左右边界都在同一块内内就暴力做
ANS[q[i].i]=calc(q[i].l,q[i].r);
continue;
}
while(r<q[i].r){
r++;
ma[a[r]]=r; //最后出现的位置
if(!st[a[r]]) st[a[r]]=r,clear[++cn]=a[r]; //st是最早出现的位置,clear是出现过的数字,用来清空数字最后出现的位置
ans=max(ans,r-st[a[r]]); //情况2
}
int tp=ans; //先保存一下,因为右区间的贡献不会被刷新,但左区间的会
while(l>q[i].l){
l--;
if(ma[a[l]]) ans=max(ans,ma[a[l]]-l);
else ma[a[l]]=l; //最后出现的位置可能在左区间中
}
ANS[q[i].i]=ans;
while(l<=br){
if(ma[a[l]]==l) ma[a[l]]=0; //去掉左区间的贡献
l++;
}
ans=tp; //去掉当前左区间的贡献
}
for(int i=1;i<=cn;i++) ma[clear[i]]=st[clear[i]]=0; //根据记录数组清空每个数出现位置的各种信息
}
for(int i=1;i<=m;i++) write(ANS[i]),puts("");
}