(笔记)莫队 回滚莫队 二次离线莫队
算法特点:
\(O(n\sqrt n)\),离线,多数时候需要一点卡常寄巧(但其时间复杂度在完全随机的数据下还是较优秀的)(?),不能做强制在线(这不废话吗),以根号分块为基础,主体为指针操作。
基础莫队
rt,以前是可以用莫队做的,但是现在加入了加强数据,好像不行了。
所以我们找了一题一样但是没有卡常的。
题意大概是给定\(A[1]\)~\(A[N]\),每次有询问\(l,r\),问区间\([l,r]\)中有多少个不同的数。
1.分块
块长为\(\sqrt n\),对询问左端点分块后对操作进行排序,第一关键字为操作左端点块的升序编号,第二关键字为操作右端点降序。
2.主体
对本题,令\(cnt[i]\)表示\(i\)出现的次数。使左指针\(le\)初始为\(1\),右指针\(ri\)为\(0\)。对排好序的操作进行指针对齐,由于之前排好序,所以在\(m,n\)同阶时,其时间复杂度为\(O(n\sqrt n)\)。
形象化即分块后右端点是有序的,每个块最多执行\(n\)次收缩操作,由\(\sqrt n\)块得最多进行\(n\sqrt n\)。
考虑左端点,每次在块内移动距离不超过\(\sqrt n\), 共\(m\)次移动,共移动\(m\sqrt n\)次,垮块的移动最多移动距离\(2\sqrt n\),最多垮\(\sqrt n\)次,故左端点共移动\(m\sqrt n + 2n\)次 ,即\(O(m\sqrt n)\)。
总体时间复杂度在两者同阶时即为\(O(n\sqrt n)\)
除此,当\(m,n\)不同阶时,使块长为\(\sqrt\frac{n^2}{m}\)时较优,具体证明可以通过式子\(\frac{n^2m}{s}=mn^2\)用基本不等式最小值推出\(ms=\frac{n^2}{s}\)时最小,整理上式可以得到\(s=\sqrt\frac{n^2}{m}\)
代码贴贴:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,m,a[N],ans[N],bs,cnt[N],temp,b[N];
struct Q{
int l,r,id;
}q[N];
bool cmp(Q x,Q y){
return b[x.l]==b[y.l] ? x.r<y.r:b[x.l]<b[y.l];
}
void add(int x){
cnt[a[x]]++;
if(cnt[a[x]]==1)temp++;
}
void del(int x){
cnt[a[x]]--;
if(cnt[a[x]]==0)temp--;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
bs=sqrt(n);
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=(i-1)/bs+1;
}
for(int i=1;i<=m;i++){
cin>>q[i].l>>q[i].r;
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++){
while(r<q[i].r)add(++r);
while(r>q[i].r)del(r--);
while(l<q[i].l)del(l++);
while(l>q[i].l)add(--l);
ans[q[i].id]=temp;
}
for(int i=1;i<=m;i++)cout<<ans[i]<<'\n';
return 0;
}
回滚莫队
用于解决:
只减不加(如 \(mex\) 值,区间内未出现过的最小非负整数),只加不减(如区间最大值)
具体前者比后者要复杂,但总体思想相同。
例题
只加不减歴史の研究
二次离线莫队
时隔不知道多少天终于来填这个坑了。
有这么一种问题,在用莫队做的时候可能在移动端点的时候需要一个数据结构来维护信息,让原本就重负难堪的莫队算法变成了 \(O(m\sqrt{n}\log n)\),难以接受,且答案的统计形式有可差分性。
具体来说,如果在普通莫队中移动右端点 \(+1\),贡献就是 \(r+1\text{对}[l,r]\text{内数的贡献}=F([l,r],r+1)=f(r,r+1)-f(l-1,r+1)\)。这个式子前面这一部分只和移动的 \(r\) 有关,可以根据 \(r\) 预处理出该结果,后面一部分考虑到 \(l\) 在移动过程中是不变的,不妨令 \(r\) 移动区间为 \(r\in [r_1,r_2]\),那么前面那个东西仍然是可以轻松前缀和求出的,后面那部分我们在 \(x=l-1\) 处加入查询对应 \(r+1\in[r_1+1,r_2+1]\) 的和,然后对 \(x\) 做扫描线。由于莫队移动是 \(O(m\sqrt{n})\) 的,我们不能点对点存否则会爆空间,这时只需存储移动区间即可,区间贡献暴力枚举每个点即可,需要注意的是应该做到 \(O(1)\) 查询,否则前面的优化都是没有意义的。
总结:我们可以考虑把这个数据结构去掉,具体来说,把每一次端点移动离线下来(做扫描线),然后用一些如值域分块之类的技巧代替数据结构,解决这些移动带来的答案改变,从而平衡复杂度。当然如果把每一个移动都点对点存下来会爆空间,所以我们只需要只存储移动的区间即可。\(m\) 次操作,\(n\) 的值域,那么存储这些移动的空间只需要 \(O(m)\) 即可。
P5047 [Ynoi2019 模拟赛] Yuno loves sqrt technology II
区间逆序对问题,感觉比板子更适合做板子。
考虑暴力,可以写普通莫队,每次移动端点时在树状数组里相应操作,复杂度多了个 \(\log\),非常不好。于是我们按照上面的说法把这些移动离线下来,例如当 \(r<q[i].r\) 移动到 \(q[i].r\),那么答案需要加上 \(\sum_{i\in [r+1,q[i].r]}[1,i]\text{中比}a_i\text{大的数的数量}-[1,l-1]\text{中比}a_i\text{大的数的数量}\),即做了个前缀和。前面那一坨可以直接树状数组 \(O(n\log n)\) 预处理,后面就可以离线下来,\(x=l-1\)。离线以后按 \(x\) 做扫描线,从 \(1\) 开始,逐次把 \([1,x]\) 中的数加入值域分块中(区间加,\(O(\sqrt{n})\)),然后回答查询的时候单点查即可 \(O(1)\)。这样做是 \(O((n+m)\sqrt{n})\) 的,同时没有给查询增加任何负担。相对地,给四种不同的端点移动分类讨论一遍就可以了。
实际上的优化就是把 \(O(m\sqrt{n}f(n))\) 变成了 \(O(m\sqrt{n}+nf(n))\) 的,在本题中这里的 \(f(n)=\log n\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5;
int n,m,a[N],b[N],bs,lsh[N],cnt;
LL Pre[N],Suf[N];
int L[N],R[N];
struct Tre{
int av[N];
int lowbit(int x){return x&-x;}
void clr(){for(int i=1;i<=cnt;i++)av[i]=0;}
void ins(int p,int x){for(int i=p;i<=cnt;i+=lowbit(i))av[i]+=x;}
int que(int p){int res=0;for(int i=p;i;i-=lowbit(i)){res+=av[i];}return res;}
}T;
struct Fq{
int val[N];
LL sum[N];
void clr(){
for(int i=1;i<=cnt;i++)
val[i]=sum[i]=0;
}
void add(int l,int r,int v){
if(b[l]==b[r]){
for(int i=l;i<=r;i++)
val[i]+=v;
}
else {
for(int i=b[l]+1;i<=b[r]-1;i++)
sum[i]+=v;
for(int i=l;i<=R[b[l]];i++)
val[i]+=v;
for(int i=L[b[r]];i<=r;i++)
val[i]+=v;
}
}
LL que(int pos){return val[pos]+sum[b[pos]];}
}P;
struct Q{int l,r,id;}q[N];
bool cmp(Q x,Q y){
return (b[x.l]!=b[y.l]?b[x.l]<b[y.l]:(b[x.l]&1?x.r<y.r:x.r>y.r));
}
vector<Q>v1[N],v2[N];
LL ans[N];
void solve(){
for(int i=1;i<=n;i++){
P.add(a[i],cnt,1);
for(Q x:v1[i]){
int l=x.l,r=x.r,id=x.id;
LL res=0;
for(int j=l;j<=r;j++)
res+=P.que(cnt)-P.que(a[j]);
if(id<0){
id=-id;
ans[id]-=res;
}
else ans[id]+=res;
}
}
P.clr();
for(int i=n;i>=1;i--){
P.add(a[i],cnt,1);
for(Q x:v2[i]){
int l=x.l,r=x.r,id=x.id;
LL res=0;
for(int j=l;j<=r;j++)
res+=P.que(a[j]-1);
if(id<0){
id=-id;
ans[id]-=res;
}
else ans[id]+=res;
}
}
}
int main(){
scanf("%d%d",&n,&m);
bs=sqrt(n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
lsh[++cnt]=a[i];
b[i]=(i-1)/bs+1;
if(!L[b[i]])L[b[i]]=i;
R[b[i]]=i;
}
sort(lsh+1,lsh+1+cnt);
cnt=unique(lsh+1,lsh+1+cnt)-(lsh+1);
for(int i=1;i<=n;i++){
a[i]=lower_bound(lsh+1,lsh+1+cnt,a[i])-lsh;
T.ins(a[i],1);
Pre[i]=i-T.que(a[i]);
Pre[i]+=Pre[i-1];
}
T.clr();
for(int i=n;i>=1;i--){
T.ins(a[i],1);
Suf[i]=Suf[i+1];
Suf[i]+=T.que(a[i]-1);
}
for(int i=1;i<=m;i++){
scanf("%d%d",&q[i].l,&q[i].r);
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++){
if(r<q[i].r){
v1[l-1].push_back((Q){r+1,q[i].r,-q[i].id});
ans[q[i].id]+=Pre[q[i].r]-Pre[r];
r=q[i].r;
}
if(l>q[i].l){
v2[r+1].push_back((Q){q[i].l,l-1,-q[i].id});
ans[q[i].id]+=Suf[q[i].l]-Suf[l];
l=q[i].l;
}
if(r>q[i].r){
v1[l-1].push_back((Q){q[i].r+1,r,q[i].id});
ans[q[i].id]-=Pre[r]-Pre[q[i].r];
r=q[i].r;
}
if(l<q[i].l){
v2[r+1].push_back((Q){l,q[i].l-1,q[i].id});
ans[q[i].id]-=Suf[l]-Suf[q[i].l];
l=q[i].l;
}
}
solve();
for(int i=1;i<=m;i++)
ans[q[i].id]+=ans[q[i-1].id];
for(int i=1;i<=m;i++)
printf("%lld\n",ans[i]);
return 0;
}
P4887 【模板】莫队二次离线(第十四分块(前体))
很多地方和上一题是一样的,不同的就是我们要想一个办法 \(O(f(n))\) 插入,\(O(1)\) 查询。考虑到 \(a_i,k<2^{14}\),只需要找到所有该范围内位数为 \(k\) 的数即可(令为 \(u\)),\(t[i]\) 表示 \(i\) 和前面 \(j\in [1,x]\) 的 \(a_j\) 有多少个异或后位数为 \(k\)。假设要插入或者查询 \(v\),插入需要找到所有 \(u\) 并将 \(t[u\oplus v]\leftarrow t[u\oplus v]+1\),插入的时候直接查询 \(t[v]\) 即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5;
int n,m,k,a[N],b[N],bs;
LL Pre[N],Suf[N];
struct Q{int l,r,id;}q[N];
bool cmp(Q x,Q y){
return (b[x.l]!=b[y.l]?b[x.l]<b[y.l]:(b[x.l]&1?x.r<y.r:x.r>y.r));
}
vector<Q>v1[N],v2[N];
LL ans[N];
int t[N];
vector<int>G;
void solve(){
for(int i=1;i<=n;i++){
for(int j:G)
t[a[i]^j]++;
for(Q x:v1[i]){
int l=x.l,r=x.r,id=x.id;
LL res=0;
for(int j=l;j<=r;j++)
res+=t[a[j]];
if(id<0){
id=-id;
ans[id]-=res;
}
else ans[id]+=res;
}
}
for(int i=0;i<16384;i++)t[i]=0;
for(int i=n;i>=1;i--){
for(int j:G)
t[a[i]^j]++;
for(Q x:v2[i]){
int l=x.l,r=x.r,id=x.id;
LL res=0;
for(int j=l;j<=r;j++)
res+=t[a[j]];
if(id<0){
id=-id;
ans[id]-=res;
}
else ans[id]+=res;
}
}
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=0;i<16384;i++)
if(__builtin_popcount(i)==k)G.push_back(i);
bs=sqrt(n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
b[i]=(i-1)/bs+1;
}
for(int i=1;i<=n;i++){
Pre[i]=Pre[i-1];
Pre[i]+=t[a[i]];
for(int j:G)
t[a[i]^j]++;
}
for(int i=0;i<16384;i++)t[i]=0;
for(int i=n;i>=1;i--){
Suf[i]=Suf[i+1];
Suf[i]+=t[a[i]];
for(int j:G)
t[a[i]^j]++;
}
for(int i=0;i<16384;i++)t[i]=0;
for(int i=1;i<=m;i++){
scanf("%d%d",&q[i].l,&q[i].r);
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++){
if(r<q[i].r){
v1[l-1].push_back((Q){r+1,q[i].r,-q[i].id});
ans[q[i].id]+=Pre[q[i].r]-Pre[r];
r=q[i].r;
}
if(l>q[i].l){
v2[r+1].push_back((Q){q[i].l,l-1,-q[i].id});
ans[q[i].id]+=Suf[q[i].l]-Suf[l];
l=q[i].l;
}
if(r>q[i].r){
v1[l-1].push_back((Q){q[i].r+1,r,q[i].id});
ans[q[i].id]-=Pre[r]-Pre[q[i].r];
r=q[i].r;
}
if(l<q[i].l){
v2[r+1].push_back((Q){l,q[i].l-1,q[i].id});
ans[q[i].id]-=Suf[l]-Suf[q[i].l];
l=q[i].l;
}
}
solve();
for(int i=1;i<=m;i++)
ans[q[i].id]+=ans[q[i-1].id];
for(int i=1;i<=m;i++)
printf("%lld\n",ans[i]);
return 0;
}

浙公网安备 33010602011771号