- 前置知识 普通莫队
当普通莫队做一些删除操作或增加操作非常困难时,回滚莫队便腾空出现,解救苍生
针对困难的操作,分为两种回滚莫队,一个是不删除莫队,一个是不增加莫队。
核心思路都是一样的 : 既然只能实现一种操作,那么就只用一种操作,剩下的交给回滚解决。
什么是回滚
回滚(Rollback)指的是程序或数据处理错误,将程序或数据恢复到上一次正确状态的行为。回滚包括程序回滚和数据回滚等类型。 —— 来自百度百科
不删除莫队
既然删除非常困难,那么就干脆删除不就好啦
例题 : 歴史の研究
发现这道题的增加操作可以\(O(1)\),只需要与当前最大值取\(\max\)即可。
但是删除操作难以\(O(1)\)维护,当把最大值删除后,无法立刻得知次大值。
于是就可以考虑回滚莫队或者其他做法了
典型的不删除莫队,大致的流程是这样的
- 输入,对序列分块,将询问按照左端点所属块的编号升序为第一关键字,右端点升序为第二关键字排序
- 处理询问,枚举左端点所属的块,将莫队的左端点指针\(l\)指向当前块右端点+1的位置,右端点指针\(r\)指向当前块的右端点。
- 如果询问的左右端点所属块相同,暴力即可
- 如果不同,拓展右指针,记录当前状态,再拓展左指针,记录答案
- 将左指针所造成的改动还原,注意不是删除操作,只是还原为拓展前所记录的状态
本来想画一个图,但由于条件不允许,于是便咕了
看看代码,应该比较好理解
点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 1e5 + 10,M = 350;
struct node{int l,r,id;}q[N];
int n,m,L[M],R[M],cnt[N],pos[N],len,a[N],c[N];
ll ans[N],res;
vector<int> num;
inline void solve(){
cin>>n>>m;
for(int i = 1;i <= n; ++i) cin>>a[i];
for(int i = 1;i <= m; ++i) cin>>q[i].l>>q[i].r,q[i].id = i;
for(int i = 1;i <= n; ++i) num.emplace_back(a[i]);
sort(num.begin(),num.end());
num.erase(unique(num.begin(),num.end()),num.end());
for(int i = 1;i <= n; ++i)
a[i] = lower_bound(num.begin(),num.end(),a[i]) - num.begin() + 1;
int V = num.size();
len = sqrt(n);
for(int i = 1;i <= len; ++i) L[i] = R[i - 1] + 1,R[i] = i*len;
if(R[len] < n) len++,L[len] = R[len - 1] + 1,R[len] = n;
for(int i = 1;i <= len; ++i) for(int j = L[i];j <= R[i]; ++j) pos[j] = i;
sort(q+1,q+1+m,[](node x,node y){return pos[x.l] == pos[y.l]?x.r<y.r:x.l<y.l;});
//以上是输入,初始化,注意排序函数
auto BF = [&](int l,int r) -> ll{
ll res = 0;
for(int i = l;i <= r; ++i) c[a[i]] ++;
for(int i = l;i <= r; ++i) res = max(res,1ll*c[a[i]]*num[a[i]-1]);
for(int i = l;i <= r; ++i) c[a[i]] --;
return res;
};//暴力代码,应该挺好懂得
auto add = [&](int x) -> void{
cnt[a[x]]++;
res = max(res,1ll*cnt[a[x]]*num[a[x]-1]);
};//O(1)的添加操作
for(int i = 1,l,r,now = 1;i <= len; ++i){
for(int j = 1;j <= V; ++j) cnt[j] = 0;
r = R[i];res = 0;
while(pos[q[now].l] == i){
l = R[i] + 1;
if(q[now].r - q[now].l < len){//如果查询在同一块内,直接暴力
ans[q[now].id] = BF(q[now].l,q[now].r);
now++;
continue;
}
while(r < q[now].r) add(++r);//拓展右指针
ll tmp = res;//记录拓展前的状态
while(l > q[now].l) add(--l);//拓展左指针
ans[q[now].id] = res;//记录答案
//还原
res = tmp;
while(l <= R[i]) --cnt[a[l++]];
now++;
}
}
for(int i = 1;i <= m; ++i) cout<<ans[i]<<'\n';
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}
例题 : 【模板】回滚莫队&不删除莫队
模板题,随便打。
但是我的记录状态的方式比较抽象。
简单说一下思路,就是记录一个值出现位置的左右端点,然后增加的时候取差即可。
点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 2e5 + 10,M = 500;
struct node{int l,r,id;}q[N];
int n,m,L[M],R[M],pos[N],len,a[N],c1[N],c2[N],lt[N],rt[N],c[N],ans[N],res;
vector<int> num;
inline void solve(){
memset(c1,0x3f,sizeof c1);
cin>>n;
for(int i = 1;i <= n; ++i) cin>>a[i];
cin>>m;
for(int i = 1;i <= m; ++i) cin>>q[i].l>>q[i].r,q[i].id = i;
for(int i = 1;i <= n; ++i) num.emplace_back(a[i]);
sort(num.begin(),num.end());
num.erase(unique(num.begin(),num.end()),num.end());
for(int i = 1;i <= n; ++i)
a[i] = lower_bound(num.begin(),num.end(),a[i]) - num.begin() + 1;
int V = num.size();
len = sqrt(n);
for(int i = 1;i <= len; ++i) L[i] = R[i - 1] + 1,R[i] = i*len;
if(R[len] < n) len++,L[len] = R[len - 1] + 1,R[len] = n;
for(int i = 1;i <= len; ++i) for(int j = L[i];j <= R[i]; ++j) pos[j] = i;
sort(q+1,q+1+m,[](node x,node y){return pos[x.l] == pos[y.l]?x.r<y.r:x.l<y.l;});
auto BF = [&](int l,int r) -> int{
int res = 0;
for(int i = l;i <= r; ++i)
c1[a[i]] = min(c1[a[i]],i),c2[a[i]] = max(c2[a[i]],i);
for(int i = l;i <= r; ++i) res = max(res,c2[a[i]] - c1[a[i]]);
for(int i = l;i <= r; ++i) c1[a[i]] = 0x3f3f3f3f,c2[a[i]] = 0;
return res;
};
auto add = [&](int x) -> void{
rt[a[x]] = max(rt[a[x]],x);
lt[a[x]] = min(lt[a[x]],x);
res = max(res,rt[a[x]] - lt[a[x]]);
};
for(int i = 1,l,r,now = 1;i <= len; ++i){
for(int j = 1;j <= V; ++j) lt[j] = 0x3f3f3f3f,rt[j] = 0;
r = R[i];res = 0;
while(pos[q[now].l] == i){
l = R[i] + 1;
if(q[now].r - q[now].l < len){
ans[q[now].id] = BF(q[now].l,q[now].r);
now++;
continue;
}
while(r < q[now].r) add(++r);
int tmp = res;
for(int j = l;j >= q[now].l;--j) c[a[j]] = lt[a[j]];
while(l > q[now].l) add(--l);
ans[q[now].id] = res;
res = tmp;
while(l <= R[i]) lt[a[l]] = c[a[l]],l++;
now++;
}
}
for(int i = 1;i <= m; ++i) cout<<ans[i]<<'\n';
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}
本来说要打这题但因为电脑死机了好不容易敲完的博客没了,于是水过去了,这篇博客是第二遍敲得
md,我要回hs
一句话题解,可撤销并查集+不删除莫队。
等模拟赛调完再打
不增加莫队
大致思路和不删除莫队一样,流程有所改变,将流程中改变的地方用粗体标注
- 输入,对序列分块,将询问按照左端点所属块的编号升序为第一关键字,右端点降序为第二关键字排序
- 处理询问,枚举左端点所属的块,将莫队的左端点指针\(l\)指向当前块左端点的位置,右端点指针\(r\)指向n。
- 如果询问的左右端点所属块相同,暴力即可
- 如果不同,拓展右指针,记录当前状态,再拓展左指针,记录答案
- 将左指针所造成的改动还原,注意不是删除操作,只是还原为拓展前所记录的状态
例题 : Rmq Problem / mex
删除只需要取\(\min\),而加入却要\(O(V)\)更改
于是就是不删除莫队了,比较简单。
点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 2e5 + 10,M = 500;
struct node{int l,r,id;}q[N];
int n,m,L[M],R[M],pos[N],len,a[N],ans[N],res,cnt[N],c[N];
vector<int> num;
inline void solve(){
int V = 0;
cin>>n>>m;
for(int i = 1;i <= n; ++i) cin>>a[i],V = max(V,a[i]);
for(int i = 1;i <= m; ++i) cin>>q[i].l>>q[i].r,q[i].id = i;
V++;
len = sqrt(n);
for(int i = 1;i <= len; ++i) L[i] = R[i - 1] + 1,R[i] = i*len;
if(R[len] < n) len++,L[len] = R[len - 1] + 1,R[len] = n;
for(int i = 1;i <= len; ++i) for(int j = L[i];j <= R[i]; ++j) pos[j] = i;
sort(q+1,q+1+m,[](node x,node y){return pos[x.l] == pos[y.l]?x.r>y.r:x.l<y.l;});
auto BF = [&](int l,int r) -> int{
int res = 0;
for(int i = l;i <= r; ++i) c[a[i]]++;
for(int i = 0;i <= V; ++i) if(!c[i]){res = i;break;}
for(int i = l;i <= r; ++i) c[a[i]]--;
return res;
};
auto del = [&](int x) -> void{
cnt[a[x]]--;
if(!cnt[a[x]]) res = min(res,a[x]);
};
for(int i = 1,now = 1,l,r;i <= len; ++i){
for(int j = 0;j <= V; ++j) cnt[j] = 0;
for(int j = L[i];j <= q[now].r; ++j) cnt[a[j]]++;
for(int j = 0;j <= V; ++j) if(!cnt[j]){res = j;break;}
r = q[now].r;
while(pos[q[now].l] == i){
l = L[i];
if(q[now].r - q[now].l <= len){
ans[q[now].id] = BF(q[now].l,q[now].r);
now++;
continue;
}
while(r > q[now].r) del(r),r--;
int tmp = res;
while(l < q[now].l) del(l),l++;
ans[q[now].id] = res;
res = tmp;
while(l > L[i]) l--,cnt[a[l]]++;
now++;
}
}
for(int i = 1;i <= m; ++i) cout<<ans[i]<<'\n';
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}
but,普通莫队真的不可做吗?
普通莫队的复杂度为\(O(q\sqrt{n}+qv)\),但是\(qv\)较小才4e10,然后要是给这个除以一个\(w\)——
bitset优化!!!
bitset中有一个成员函数,为\(_Find_next(int prev)\),作用为查询\(prev\)下一个为\(true\)的位置,返回下标。
然后就可以暴力跑了。时间复杂度\(O(n\sqrt{n}+\frac{qv}{w})\),算出来大概是\(7e8\)的,然后因为常数小,跑不满,再加上一堆玄学优化,轻微卡常,就过了……
跑得比上面那篇回滚莫队要快
点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 2e5 + 10,M = 500;
struct node{int l,r,id;}q[N];
int n,m,a[N],pos[N],L[M],R[M],len,ans[N],cnt[N];
bitset<N> have;
inline void solve(){
cin>>n>>m;
for(int i = 1;i <= n; ++i) cin>>a[i];
for(int i = 1;i <= m; ++i) cin>>q[i].l>>q[i].r,q[i].id = i;
len = sqrt(n);
for(int i = 1;i <= len; ++i) L[i] = R[i - 1] + 1,R[i] = i*len;
if(R[len] < n) len++,L[len] = R[len - 1] + 1,R[len] = n;
for(int i = 1;i <= len; ++i) for(int j = L[i];j <= R[i]; ++j) pos[j] = i;
sort(q+1,q+1+m,[](node x,node y){return pos[x.l] == pos[y.l]?(pos[x.l]&1?x.r<y.r:x.r>y.r):x.l<y.l;});
have.set();
int l = 1,r = 0,res = 0;
auto add = [&](int x) -> void{
cnt[a[x]]++;
have.reset(a[x]);
if(a[x] == res) res = have._Find_next(a[x]);
};
auto del = [&](int x) -> void{
cnt[a[x]]--;
if(!cnt[a[x]]) have.set(a[x]),res = min(res,a[x]);
};
for(int i = 1;i <= m; ++i){
int left = q[i].l,right =q[i].r;
while(l > left) l--,add(l);
while(r < right) r++,add(r);
while(l < left) del(l),l++;
while(r > right) del(r),r--;
ans[q[i].id] = res;
}
for(int i = 1;i <= m; ++i) cout<<ans[i]<<'\n';
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}
时间复杂度证明 :
假设块长为\(b\)
如果左右端点在同一块中,为\(O(b)\)
反之,移动右端点的复杂度为\(O(n)\),由于左端点单次移动不会超过\(b\),有\(\frac{n}{b}\)个块
所以时间复杂度为\(O(mb+\frac{n^2}{b})\),块长取\(b=\frac{n}{\sqrt{m}}\)时最优,复杂度为\(O(n\sqrt{m})\)
没有太多的例题,主要是实在没时间打