BFqwq
2020-10-29 20:25:18
前置知识:莫队。
不会的同学可以看往期日报
回滚莫队,听起来好像很高级的样子。
但其实,它还有个很平凡的名字——不删除莫队。
回顾普通的莫队,其实说到底一共只有四个操作,分别是
而这些操作实际上又分成两大类,分别是插入和删除。
简单的说,当我们执行完
而另外两个操作之后呢,我们的区间变短了,相当于我们从区间中删除了一个数。
虽然在平时的莫队中,这两者的复杂度是一样的,但显然,删除的操作往往更为复杂,甚至有的时候我们能很简单的完成插入操作,但却一直想不到如何进行删除。这个时候,我们的回滚莫队就要登场了。
我们先通过一个比较简单的例子来了解一下回滚莫队。
给定一个序列以及若干次询问,求区间众数,可离线。
这个问题是个经典的问题,目前有许多的解法。但从思维难度上来讲,回滚莫队可能是最简单的。
考虑莫队。我们维护一个
但要删除一个数就显得比较复杂,于是我们可以对莫队作一点变化。
首先,对于左右端点位于一个块内的询问,我们直接暴力,这样的复杂度显然正确。
接着,我们依然按照左端点排序,将左端点在同一个块内的数拿出来一起处理。
我们直接将右指针移到该左端点所在块的右端点处,然后暴力向右插入。这样,在块外的部分的贡献我们就可以统计了。
然后我们将状态保存,然后将左指针移到左端点所在块的右端点
然后,我们再将左端点逐个移回,并撤销左区间的贡献即可。注意这里是撤销,而不是删除。
具体的实现方法很多,比如我们在右端点时将区间众数答案保存(定义一个变量
这样,我们的
右端点的复杂度之和显然是
以上就是回滚莫队的基本内容,通过巧妙的变换莫队的指针位置来避免了删除操作。
当然实际上我们也可以类似的弄一个不插入莫队(这个名字是自己取的qaq),但由于一般很少见到插入比删除麻烦的题,所以实际作用不大。
下面我们来做几道题练练手。
https://www.luogu.com.cn/problem/AT1219
实际上,这个题和区间众数的区别不是很大,只不过是在更新答案的时候再乘以一个
和区间众数一样,我们维护一个数的出现次数。然后跑回滚莫队。更新答案时用事件的发生次数乘以重要度,与目前答案取最大值作为答案。
#include <bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
register int x=0;
register bool f=0;
register char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') f=1;
c=getchar();
}
while(c>='0'&&c<='9'){
x=(x<<3)+(x<<1)+c-48;
c=getchar();
}
return f?-x:x;
}
char cr[200];int tt;
inline void print(int x,char k='\n') {
if(!x) putchar('0');
if(x < 0) putchar('-'),x=-x;
while(x) cr[++tt]=x%10+'0',x/=10;
while(tt) putchar(cr[tt--]);
putchar(k);
}
const int maxn=1e6+10;
const int blk=405;
int n,a[maxn],m,bl[maxn],cnt[maxn];
int L[maxn],R[maxn],ans[maxn],mx,lst;
int lsh[maxn],len,c[maxn],del;
bool flag=0;
struct query{
int l,r,id;
friend bool operator <(query c,query d){
if(bl[c.l]==bl[d.l]) return c.r<d.r;
return c.l<d.l;
}
}q[maxn];
int chk(int l,int r){
mx=0;
for(int i=l;i<=r;i++){
cnt[c[i]]++;
mx=max(a[i]*cnt[c[i]],mx);
}
for(int i=l;i<=r;i++) cnt[c[i]]--;
return mx;
}
signed main(){
n=read();m=read();
int lxl=1;
L[1]=1;
while(L[lxl]+blk<n){
R[lxl]=L[lxl]+blk-1;
lxl++;
L[lxl]=R[lxl-1]+1;
}
R[lxl]=n;
for(int i=1;i<=lxl;i++){
for(int j=L[i];j<=R[i];j++){
bl[j]=i;
}
}
for(int i=1;i<=n;i++){
a[i]=lsh[i]=c[i]=read();
}
sort(lsh+1,lsh+n+1);
len=unique(lsh+1,lsh+n+1)-lsh-1;
for(int i=1;i<=n;i++)
c[i]=lower_bound(lsh+1,lsh+len+1,c[i])-lsh;
for(int i=1;i<=m;i++){
q[i].l=read();q[i].r=read();q[i].id=i+del;
if(bl[q[i].l]==bl[q[i].r]){
ans[i+del]=chk(q[i].l,q[i].r);
i--,m--,del++;
}
}
sort(q+1,q+m+1);
int l,r;
for(int i=1;i<=m;i++){
if(bl[q[i].l]!=bl[q[i-1].l]||i==1) flag=1;
if(flag){
memset(cnt,0,sizeof(cnt));
r=R[bl[q[i].l]];mx=lst=0;
flag=0;
}
while(r<q[i].r){
r++;
cnt[c[r]]++;
mx=lst=max(lst,cnt[c[r]]*a[r]);//lst代表答案存档
}
l=R[bl[q[i].l]]+1;
while(l>q[i].l){
l--;
cnt[c[l]]++;
mx=max(mx,cnt[c[l]]*a[l]);
}
ans[q[i].id]=mx;
mx=lst;//答案先更新回去
l=R[bl[q[i].l]]+1;
while(l>q[i].l){
l--;
cnt[c[l]]--;//左边部分出现次数减去
}
}
for(int i=1;i<=m+del;i++) print(ans[i]);
return 0;
}
考虑维护每个数最左、最右出现的位置。
在右移指针的时候,直接更新最右出现的位置,然后如果目前还没有出现过这个数,就更新最左出现的位置,否则用右减左更新答案。这样,我们就得到了右边区间的贡献。
然后保存答案,移动左指针。在移动左指针时,我们只需要更新最右端点(如果这个数在右边没有出现过),不需要更新最左端点,因为目前左指针的点一定是区间中最左的点。
当我们更新完答案之后,我们对左指针的移动进行撤销操作。由于我们只更新了最右端点,并且是只更新了右侧没有出现过的数的右端点,所以我们只需要对这类数进行撤销。具体的操作就是:
if(mx[a[l]]==l){
mx[a[l]]=0;
}
然后在每个块结束的时候,要对目前的最左最右两个数组作清除操作。
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
int read(){
bool f=1;
int x=0;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-')f=0;
c=getchar();
}
while(c>='0'&&c<='9'){
x=(x<<3)+(x<<1)-48+c;
c=getchar();
}
return f?x:x*-1;
}
char cr[200];int tt;
inline void print(register int x,register char k='\n') {
if(!x) putchar('0');
if(x < 0) putchar('-'),x=-x;
while(x) cr[++tt]=x%10+'0',x/=10;
while(tt) putchar(cr[tt--]);
putchar(k);
}
const int maxn=2e5+10;
const int blk=460;
int L[maxn],R[maxn],bl[maxn],l,r,len;
int n,m,a[maxn],del,res,lsh[maxn];
int mn[maxn],mx[maxn],ans[maxn],clr[maxn];
struct que{
int l,r,id;
friend bool operator <(que x,que y){
return bl[x.l]^bl[y.l]?x.l<y.l:x.r<y.r;
}
}q[maxn];
signed main(){
n=read();
int lxl=1;
L[1]=1;
while(L[lxl]+blk<n){
R[lxl]=L[lxl]+blk-1;
lxl++;
L[lxl]=R[lxl-1]+1;
}
R[lxl]=n;
for(int i=1;i<=lxl;i++){
for(int j=L[i];j<=R[i];j++){
bl[j]=i;
}
}
for(int i=1;i<=n;i++){
a[i]=lsh[i]=read();
}
sort(lsh+1,lsh+n+1);
len=unique(lsh+1,lsh+n+1)-lsh-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(lsh+1,lsh+len+1,a[i])-lsh;
}
memset(mn,0x3f,sizeof(mn));
m=read();
for(int i=1;i<=m;i++){
q[i].l=read();q[i].r=read();
q[i].id=i+del;
if(bl[q[i].l]==bl[q[i].r]){
for(int j=q[i].l;j<=q[i].r;j++){
if(mn[a[j]]<j){
ans[q[i].id]=max(ans[q[i].id],j-mn[a[j]]);
}
else mn[a[j]]=j;
}
for(int j=q[i].l;j<=q[i].r;j++){
mn[a[j]]=inf;
}
del++;i--;m--;
}
}
sort(q+1,q+m+1);
for(int k=1,i=1;k<=lxl;k++){
l=R[k]+1,r=R[k],res=0;
while(bl[q[i].l]==k){
while(r<q[i].r){
r++,mx[a[r]]=r;
if(mn[a[r]]==inf){
mn[a[r]]=r;
}
else{
res=max(res,r-mn[a[r]]);
}
}
ans[q[i].id]=res;
while(l>q[i].l){
l--;
if(mx[a[l]]){
ans[q[i].id]=max(ans[q[i].id],mx[a[l]]-l);
}
else mx[a[l]]=l;
}
while(l<=R[k]){
if(mx[a[l]]==l){
mx[a[l]]=0;
}
l++;
}
i++;
}
memset(mn,0x3f,sizeof(mn));
memset(mx,0,sizeof(mx));
}
for(int i=1;i<=m+del;i++){
print(ans[i]);
}
return 0;
}
其实,这个题和上一题区别并不大。
我们先对数组做个前缀和,然后我们发现,
注意这里是从
不同的是,前缀和可能会是负数,因此我们可以加一个相同的数,使其变为正数,就可以放到数组维护了。
另外,如果我们查询的区间是
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
int read(){
bool f=1;
int x=0;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-')f=0;
c=getchar();
}
while(c>='0'&&c<='9'){
x=(x<<3)+(x<<1)-48+c;
c=getchar();
}
return f?x:x*-1;
}
char cr[200];int tt;
inline void print(register int x,register char k='\n') {
if(!x) putchar('0');
if(x < 0) putchar('-'),x=-x;
while(x) cr[++tt]=x%10+'0',x/=10;
while(tt) putchar(cr[tt--]);
putchar(k);
}
const int maxn=52233;
const int blk=320;
int L[maxn],R[maxn],bl[maxn],st[maxn],l,r;
int n,m,a[maxn],del,top,res;
int mn[maxn<<1],mx[maxn<<1],ans[maxn],clr[maxn];
struct que{
int l,r,id;
friend bool operator <(que x,que y){
return bl[x.l]^bl[y.l]?x.l<y.l:x.r<y.r;
}
}q[maxn];
signed main(){
n=read();m=read();
int lxl=1;
L[1]=1;
while(L[lxl]+blk<n){
R[lxl]=L[lxl]+blk-1;
lxl++;
L[lxl]=R[lxl-1]+1;
}
R[lxl]=n;
for(int i=1;i<=lxl;i++){
for(int j=L[i];j<=R[i];j++){
bl[j]=i;
}
}
a[0]=maxn;
for(int i=1;i<=n;i++){
a[i]=read()+a[i-1];
}
memset(mn,0x3f,sizeof(mn));
for(int i=1;i<=m;i++){
q[i].l=read();q[i].r=read();
q[i].id=i+del;
if(bl[q[i].l]==bl[q[i].r]){
mn[a[q[i].l-1]]=q[i].l-1;
for(int j=q[i].l;j<=q[i].r;j++){
if(mn[a[j]]<j){
ans[q[i].id]=max(ans[q[i].id],j-mn[a[j]]);
}
else mn[a[j]]=j;
}
mn[a[q[i].l-1]]=inf;
for(int j=q[i].l;j<=q[i].r;j++){
mn[a[j]]=inf;
}
del++;i--;m--;
}
}
sort(q+1,q+m+1);
for(int k=1,i=1;k<=lxl;k++){
l=R[k]+1,r=R[k],res=0,top=0;
while(bl[q[i].l]==k){
while(r<q[i].r){
r++,mx[a[r]]=r;
if(mn[a[r]]==inf){
mn[a[r]]=r,st[top++]=a[r];
}
else{
res=max(res,r-mn[a[r]]);
}
}
ans[q[i].id]=res;
while(l>=q[i].l){
l--;
if(mx[a[l]]){
ans[q[i].id]=max(ans[q[i].id],mx[a[l]]-l);
}
else mx[a[l]]=l;
}
while(l<=R[k]){
if(mx[a[l]]==l){
mx[a[l]]=0;
}
l++;
}
i++;
}
while(top--){
mn[st[top]]=inf,mx[st[top]]=0;
}
}
for(int i=1;i<=m+del;i++){
print(ans[i]);
}
return 0;
}
理论上来说,回滚莫队由于不用删除,所以可以替代所有的普通莫队。由于其思路简单,所以在实际应用的时候可能有一定的优势。
但目前的 OI 中,直接使用回滚莫队进行操作的题目并不多,大部分的题目都可以用普通莫队解决,所以这一算法的实用性不是很强。
最后,这里给出两道个人认为比较有难度的回滚莫队题,有兴趣的同学可以自行思考。