回滚莫队
作者:互联网
我们发现有的时候我们莫队不方便维护加或减中的一个,只方便维护另一个,我们就要考虑另外一种更加针对化的莫队。
回滚莫队
先开一个例题 「JOISC 2014 Day1」历史研究
题目大意:一个序列,多个询问。每次询问一段区间 \([l,r]\) ,定义重要度为一个数出现次数与其权值的乘积,求区间中的重要度的最大值。
我们发现这道题中我们并不方便维护删除。因为我们如果加入一个数,那么显然只要维护当前最大值与加入的数
的重要度的较大者即可,这是 \(O(1)\) 的。而删除我们并不知道当最大值被删除后我们应选择谁,所以难以 \(O(1)\) 维护。那么我们怎么考虑不维护删除操作呢?
我们先按照左端点所属块从小到大排序,右端点第二关键字从小到大排序。我们对于所有左端点在同一个块内的,我们将左端点控制在当前块的右边界 \(+1\) 的位置,右端点为当前块的左边界。这样我们维护了一个空集。当然网上还有一种写法是右端点位于右边界,然后对于左右询问端点同块的直接跑暴力,这样也是可以的,其实区别不大。
这样设置好之后,我们先把右端点向着询问靠拢,更新答案。之后我们临时创建 \(l2,res2\) ,初始值为 \(l,res\) ,然后我们把 \(l2\) 向询问靠拢同时更新 \(res2\)。当前询问的答案就是 \(res2\) ,之后我们恢复被 \(l2\) 加入的部分。(注意并未真正意义上的删除)为什么我们这样做?因为我们的排序方法虽然保证了右端点的单调性,但是没法保证左端点,因此我们每次都刷一遍来维护左端点。
关于复杂度是 \(O(n\sqrt n)\)。证明一下:首先对于每个块内,右端点单调增,总共一整个块的复杂度是 \(O(n)\) ,对于所有块就是 \(O(n\sqrt n)\) 。左端点每次询问最多跑 \(O(\sqrt n)\) ,总共算起来就是 \(O(n\sqrt n)\) 。每次切换左端点的块,右端点最多跑 \(O(n)\) ,总共 \(O(n\sqrt n)\)。所以总复杂度是 \(O(n\sqrt n)\) 。
参考代码
#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
template<class T>
inline void read(T &s){
s=0;char ch=gc();bool f=0;
while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
if(ch=='.'){
db p=0.1;ch=gc();
while('0'<=ch&&ch<='9') {s=s+p*(ch^48);ch=gc();}
}
s=f?-s:s;
}
template<class T,class ...A>
inline void read(T &s,A &...a){
read(s);read(a...);
}
inline bool blank(char c){
return c==' ' or c=='\t' or c=='\n' or c=='\r' or c==EOF;
}
inline void gs(std::string &s){
s+='#';char c=gc();
while(blank(c) ) c=gc();
while(!blank(c) ){
s+=c;c=gc();
}
}
};
using IO::read;
using IO::gs;
const int N=1e5+3;
const int BLK=317;
int n,Q,num;
int a[N],c[N];
int st[BLK],ed[BLK];
int belong[N];
struct ques{
int id,l,r;
}qu[N];
namespace Discrete{
int id[N];
inline void work(){
for(int i=1;i<=n;++i){
id[i]=i;
}
std::sort(id+1,id+1+n,[](int x,int y){
return a[x]<a[y];
});
int top=1,last=a[id[1] ];
a[id[1] ]=1;
for(int i=2;i<=n;++i){
if(a[id[i] ]!=last) ++top;
last=a[id[i] ];
c[top]=a[id[i] ];a[id[i] ]=top;
}
}
};
ll ans[N];
int cnt[N],cnt2[N];
inline void add(int x,ll &res){
++cnt[a[x] ];
res=std::max(res,1ll*cnt[a[x] ]*c[a[x] ]);
}
inline void del(int x){
--cnt[a[x] ];
}
int blk;
int main(){
//filein(a);fileot(a);
read(n,Q);
blk=sqrt(n);
for(int i=1;i<=n;++i){
read(a[i]);
}
//分块
num=n/blk;
for(int i=1;i<=num;++i){
st[i]=(i-1)*blk+1;
ed[i]=i*blk;
}
if(ed[num]!=n){
++num;
st[num]=ed[num-1]+1;
ed[num]=n;
}
for(int i=1;i<=num;++i){
for(int l=st[i];l<=ed[i];++l){
belong[l]=i;
}
}
for(int i=1;i<=Q;++i){
int l,r;
read(l,r);
qu[i]={i,l,r};
}
std::sort(qu+1,qu+1+Q,[](ques x,ques y){
if(belong[x.l]==belong[y.l])
return x.r<y.r;
return belong[x.l]<belong[y.l];
});
//离散化
Discrete::work();
//回滚莫队
int lab=0;
int l=1,r=0;ll mx=0;
for(int i=1;i<=Q;++i){
int ql=qu[i].l,qr=qu[i].r;
if(lab!=belong[ql]){
while(r>st[belong[ql] ]) del(r--);
while(l<ed[belong[ql] ]+1) del(l++);
lab=belong[ql];
mx=0;
}
while(r<qr) add(++r,mx);
int l2=l;ll mx2=mx;
while(ql<l2) add(--l2,mx2);
ans[qu[i].id]=mx2;
while(l2<l) del(l2++);
}
for(int i=1;i<=Q;++i){
printf("%lld\n",ans[i]);
}
return 0;
}
题目大意:一个序列,多个询问。每次询问一段区间 \([l,r]\) ,求区间 \(mex\) ,也即区间内最小没出现过的自然数。
这次我们发现删除要更加容易维护,所以考虑不做加入操作。我们先按照左端点所在块从小到大排序,再以右端点为第二关键字从大到小排序。
对于每个块,我们考虑将左右端点分别放在询问左端点块的左边界和询问右端点的右边界。这样我们属于是预处理了当前块内询问的完全覆盖集,最后我们向中间删即可。但是由于左端点仍然没有单调性,于是我们还是临时的删除,当前询问结束就恢复。
具体来说,我们对于每个块的询问先预处理出最大集和答案,然后随着删除的时候更新即可。预处理的答案我们暴力求解,然后删除的时候如果一个数被删空了,就可以和答案取较小者。
复杂度证明:
所有的东西都和上面那个题一样,只需要证暴力预处理答案不会寄即可。
发现暴力处理是 \(O(n)\) 的,但是换块只会进行 \(O(\sqrt n)\) ,因此总复杂度还是 \(O(n\sqrt n)\).
参考代码
#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
template<class T>
inline void read(T &s){
s=0;char ch=gc();bool f=0;
while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
if(ch=='.'){
db p=0.1;ch=gc();
while('0'<=ch&&ch<='9') {s=s+p*(ch^48);ch=gc();}
}
s=f?-s:s;
}
template<class T,class ...A>
inline void read(T &s,A &...a){
read(s);read(a...);
}
inline bool blank(char c){
return c==' ' or c=='\t' or c=='\n' or c=='\r' or c==EOF;
}
inline void gs(std::string &s){
s+='#';char c=gc();
while(blank(c) ) c=gc();
while(!blank(c) ){
s+=c;c=gc();
}
}
};
using IO::read;
using IO::gs;
const int N=2e5+3;
const int BLK=448+3;
const int inf=1e9;
int n,m;
int a[N];
struct ques{
int id,l,r;
}qu[N];
int cnt[N],cnt2[N];
int blk,num;
int st[BLK],ed[BLK];
int belong[N];
inline void add(int x){
++cnt[a[x] ];
}
inline void del(int x){
--cnt[a[x] ];
}
inline void del(int x,int &mex){
--cnt[a[x] ];
if(!cnt[a[x] ]){
mex=std::min(mex,a[x]);
}
}
int ans[N];
int main(){
//filein(a);fileot(a);
read(n,m);
for(int i=1;i<=n;++i){
read(a[i]);
}
blk=sqrt(n);
num=n/blk;
for(int i=1;i<=num;++i){
st[i]=ed[i-1]+1;
ed[i]=i*blk;
}
if(ed[num]!=n){
++num;
st[num]=ed[num-1]+1;
ed[num]=n;
}
for(int i=1;i<=num;++i){
for(int l=st[i];l<=ed[i];++l){
belong[l]=i;
}
}
for(int i=1;i<=m;++i){
int l,r;
read(l,r);
qu[i]={i,l,r};
}
std::sort(qu+1,qu+1+m,[](ques x,ques y){
if(belong[x.l]==belong[y.l]){
return x.r>y.r;
}
return belong[x.l]<belong[y.l];
});
int l=1,r=0;
int mex=0;
int lab=0;
for(int i=1;i<=m;++i){
int ql=qu[i].l,qr=qu[i].r;
if(lab!=belong[ql]){
while(r<ed[belong[qr] ]) add(++r);
while(l<st[belong[ql] ]) del(l++);
mex=0;
while(cnt[mex]) ++mex;
lab=belong[ql];
}
while(qr<r){
del(r--,mex);
}
int l2=l,mex2=mex;
while(l2<ql){
del(l2++,mex2);
}
ans[qu[i].id]=mex2;
while(l2>l){
add(--l2);
}
}
for(int i=1;i<=m;++i){
printf("%d\n",ans[i]);
}
//fprintf(stderr,"%dms\n",clock() );
return 0;
}
这个就可以练练手。不过之后也会发题解就是了。
标签:回滚,int,read,gc,端点,inline,莫队,define 来源: https://www.cnblogs.com/cbdsopa/p/16025385.html