莫队
莫队算法(Mo's algorithm)莫涛队长发明的算法,尊称莫队。
先膜一下莫队(\%\%\%)莫涛 - 知乎
思路A:two pointers处理
two pointers处理
是一种优美的暴力。
例如此题:P3901 数列找不同
现有数列 (A_1,A_2,ldots,A_N),(M)个询问 ((L_i,R_i)),询问 (A_{L_i} ,A_{L_i+1},ldots,A_{R_i})是否互不相同。
(N,Mle 10^5)
(以下取(N,M)同阶)
扩展一下这个问题,变成"求((L{i},R_i))区间内有多少对数字相同".
如果完完全全暴力的话,开类似于桶排序的桶,每次在((L,R))的区间内更新桶,并计算答案,复杂度(O(N^3)).
用莫队①优化一下这个过程:
- 用两个指针(pl)和(pr),两个指针所指的标号内是我们维护的区间。于是我们可以仅移动这两个指针来维护区间。
- 每一步移动的复杂度是(O(1)).
- 用(pans)表示这个区间内的答案
对于移动指针的过程,假设要将指针移动到((l,r)):
- 对于移动左指针((pl o l))
- ①若(pl > l),表示当前区间左端点短所求,则一步步向左移动左端点。
- 此时:每移动左端点一次,相当于向区间内增加一个(a[pl-1]),对答案的贡献为当前已有的(a[pl-1])的个数,即(buc[a_{[pl-1]}]).
- 故此处为
add(--pl)
.
- ②若(pl<l),表示当前区间左端点长于所求,则一步步向右移动左端点。
- 此时:每移动左端点一次,相当于向区间内减少一个(a[pl]),对答案的贡献为当前已有的(a[pl])的个数(-1),即(buc[a_{[pl]}])(-1).
- 故此处为
del(pl++)
.
- ①若(pl > l),表示当前区间左端点短所求,则一步步向左移动左端点。
- 对于移动右指针((pr o r))
- 若(pr < r),与上①相同
- 为
add(++pr)
.
- 为
- 若(pr > r),与上②相同
- 为
del(pr--)
.
- 为
- 若(pr < r),与上①相同
- 关于
add
:在计算后要将(buc++),故为qans += buc[a[k]]++
. - 关于
del
:先(-1)再计算,故为qans -= --buc[a[k]];
将每一次进行add()
或del()
称为一次扩展。
另外,(pl)和(pr)初值应赋为(1)和(0),此时表示扩展内没有任何元素。
关于扩展的代码:
ll pans;int pl=1,pr;
inline void add(int k){pans += buc[a[k]]++;}
inline void del(int k){pans -= --buc[a[k]];}
inline int calc(int l,int r){
while(pl > l)add(--pl);
while(pr < r)add(++pr);
while(pl < l)del(pl++);
while(pr > r)del(pr--);
return pans;
}
此时一次扩展的复杂度是(O(1))的。
这样可以将复杂度压缩到(O(M*N)).
但是它还会TLE。如何进一步优化?
思路A+
对于以上算法,复杂度的上限是什么?
假设有以下(N=10^5)的((L_i,R_i))查询数据:
1 1
100000 100000
1 1
100000 100000
………
会发生什么?
每次操作,都会将(pl:1 o Nspace ,space pr:1 o N)或(pl:N o 1space,space pr:N o1)。
这样会出现很多很多次无用的扩展。
考虑如何优化这些扩展:
莫队的思路是分块。
取正整数(B),把序列每(B)个分为一段,即分段长度是(B).
对于左端点在同一块内的相邻询问,右端点递增,一共至多扩展(N) 次,左端点每次询问至多扩展(B) 步;对于左端点不在同一块内的相邻询问,至多有(frac nB) 个。复杂度为(O(Nfrac NB + BN)) 次扩展的复杂度。
利用均值不等式,(Nfrac NB+BN ge2sqrt{Nfrac NB*BN}=2Nsqrt N),当且仅当 $B =sqrt N $时等号成立。
故取(B=sqrt N),最优复杂度为(O(Nsqrt N)) 次扩展的复杂度。
这样分块后,对于询问的区间,按左端点所在块的编号为第一关键字,右端点为第二关键字从小到大排序。按顺序求每个区间的答案,每次直接从上一个区间暴力扩展移动到这个区间。
所以可以知道:莫队一定是离线的。
代码实现:
开一个(bel[i])数组记录(i)所在块的编号,易得(bel[i] = frac i B + 1).
用结构体记录每次查询的区间信息和编号。
按以上方法排序后,每次操作将所得值还原为原顺序最后输出即可。
struct ask{int l,r,id;}q[N];
inline bool operator < (const ask a,const ask b){return bel[a.l] != bel[b.l] ? a.l < b.l : a.r < b.r;}
bool ans[N];
main:{
len = sqrt(n);
for(int i:1->n)
bel[i] = i / len + 1;
for(int i:1->m)
q[i].l = read() , q[i].r = read() , q[i].id = i;
sort(q+1,q+m+1);
for(int i:1->m)
ans[q[i].id]= calc(q[i].l,q[i].r);
for(int i:1->m)
print(ans[i]);
}
总复杂度为(O(Nsqrt N)).
数列找不同AC代码:
#include <bits/stdc++.h>
#define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout);
using namespace std;
const int INF = 0x3f3f3f3f,N = 1e5+5;
typedef long long ll;
typedef unsigned long long ull;
inline ll read(){
ll ret = 0 ;char ch = ' ' , c = getchar();
while(!(c >= '0' && c <= '9'))ch = c , c = getchar();
while(c >= '0' && c <= '9')ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
return ch == '-' ? -ret : ret;
}
int n,m;
int a[N],buc[N],bel[N];
int len;
ll qans;int pl=1,pr;
inline void add(int k){qans += buc[a[k]]++;}
inline void del(int k){qans -= --buc[a[k]];}
inline int calc(int l,int r){
while(pl > l)add(--pl);
while(pr < r)add(++pr);
while(pl < l)del(pl++);
while(pr > r)del(pr--);
return qans;
}
struct ask{int l,r,id;}q[N];
inline bool operator < (const ask a,const ask b){return bel[a.l] != bel[b.l] ? a.l < b.l : a.r < b.r;}
bool ans[N];
signed main(){
n = read() , m = read();
len = ceil(sqrt(n));
for(int i = 1 ; i <= n ; i ++)
a[i] = read(),
bel[i] = i / len + 1;
for(int i = 1 ; i <= m ; i ++)
q[i].l = read() , q[i].r = read() , q[i].id = i;
sort(q+1,q+m+1);
for(int i = 1 ; i <= m ; i ++){
int l = q[i].l , r = q[i].r , id = q[i].id;
ans[id]= !calc(l,r);
}
for(int i = 1 ; i <= m ; i ++)
printf("%s
",ans[i]?"Yes":"No");
return 0;
}
基本的莫队已经结束了。我们来看一道例题:P1494 [国家集训队] 小Z的袜子
给定序列(A_n),每次询问查询((L_i,R_i))区间内任选两个数,选到相同数字的概率。
(nle5 imes 10^5)
把上面一题的扩展"求((L{i},R_i))区间内有多少对数字相同"稍作修改即可。
答案为(frac{calc(l,r)}{C^2_{r-l+1}})。
约分时,上下同除(gcd)就可以。
#include <bits/stdc++.h>
#define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout);
using namespace std;
const int INF = 0x3f3f3f3f,N = 5e4+5;
typedef long long ll;
typedef unsigned long long ull;
inline ll read(){
ll ret = 0 ;char ch = ' ' , c = getchar();
while(!(c >= '0' && c <= '9'))ch = c , c = getchar();
while(c >= '0' && c <= '9')ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
return ch == '-' ? -ret : ret;
}
int n,m;
int a[N],buc[N],bel[N],len;
ll qans;int pl=1,pr;
inline void add(int k){qans += buc[a[k]]++;}
inline void del(int k){qans -= --buc[a[k]];}
inline int calc(int l,int r){
while(pl > l)add(--pl);
while(pr < r)add(++pr);
while(pl < l)del(pl++);
while(pr > r)del(pr--);
// printf("(%d,%d):%lld
",pl,pr,ans);
return qans;
}
struct ask{int l,r,id;}q[N];
inline bool operator < (ask a,ask b){return bel[a.l] != bel[b.l] ? a.l < b.l : a.r < b.r;}
ll gcd(ll x,ll y){return y ? gcd(y,x%y):x;}
ll ans[N][2];
signed main(){
n = read() , m = read();
len = ceil(sqrt(n));
for(int i = 1 ; i <= n ; i ++)
a[i] = read(),
bel[i] = i/len + 1;
for(int i = 1 ; i <= m ; i ++)
q[i].l = read() , q[i].r = read() , q[i].id = i;
sort(q+1,q+m+1);
for(int i = 1 ; i <= m ; i ++){
int l = q[i].l , r = q[i].r , id = q[i].id;
if(l == r){ans[id][0] = 0,ans[id][1] = 1;continue;}
ans[id][0] = calc(l,r),ans[id][1] = 1ll * (r-l+1) * (r-l) / 2;
ll k = gcd(ans[id][0],ans[id][1]);
ans[id][0] /= k,ans[id][1] /= k;
}
for(int i = 1 ; i <= m ; i ++)
printf("%lld/%lld
",ans[i][0],ans[i][1]);
return 0;
}
优化
对于普通的莫队,还是存在一些可优化的地方的。下面我们来具体分析。
奇偶优化
如图(红箭头表示(pl)的移动,绿箭头表示(pr)的移动)
可以发现,对于左端点进入新的一个区间时,右端点需要从(N)处回到最左边,再跑回到最右边。这样也是进行了很多次多余的扩展。
提供一种奇偶优化的方案:
对于分块标号为奇数的,按(p[i].r)升序排列,反之偶数按(p[i].r)降序排列。效果如下:
只需将排序一处的代码修改:
- 如果(a.l)与(b.l)在同一个块内,则:
- 如果所在块编号是奇数,则按(a.l<b.l)排列;
- 如果所在块编号是偶数,则按(a.l>b.l)排列。
- 如果不在同一个块内,则左端点编号小的在前。
所以代码为:
inline bool operator < (const ask a,const ask b){return bel[a.l] == bel[b.l] ? (bel[a.l] & 1 ? a.r < b.r : a.r > b.r): a.l < b.l;}
这个优化是优化在常数,不过可以使大数据快一倍。
带修莫队
前面我们说到,莫队一定是离线的。但是如果遇到一些题目,要求修改?
例如【P1903 [国家集训队]数颜色 / 维护队列】
有(A_N)的序列和(M)次操作,每次操作进行查询((L,R))区间内不同的数字个数或将(A_p)修改为(x).
(N,M le 1.5*10^5).
题里要求必须支持修改。
我们来考虑如何修改。
对于普通的莫队,我们的操作是:
- 每次维护(pl)与(pr)和((L,R))的位置。
那么对于修改呢?
莫队提供的方案是:增加一个时间轴(T).
- 可以增加一个变量(now),表示当前处理的询问之前执行过多少次修改。
- 对于每一次查询,记录当前查询之前进行多少修改。
这样,可以把每次扩展改成:
inline int calc(int l,int r,int id,int t){
while(pl > l)add(--pl);
while(pr < r)add(++pr);
while(pl < l)del(pl++);
while(pr > r)del(pr--);
while(now < t)mod(++now,id);
while(now > t)mod(now--,id);
return pans;
}//其中的6,7行为带修莫队新增。
其中,我们在扩展需要额外传入(id,t)两个参量。
对于(mod)(modify)函数,写成如下:
inline void mod(int k,int id){
if(q[id].l <= mo[k].p && mo[k].p <= q[id].r)
pans ...... ;
swap(mo[k].v,a[mo[k].p]);
}
首先,所对应的修改位置在当前查询的区间内才需要修改(pans). 修改(pans)的操作因题而定。
比如:例题,则为pans += !buc[mo[k].v]++ , pans -= !--buc[a[mo[k].p]];
,表示分别对修改前和修改后进行答案贡献计算。
最后的swap
比较巧妙:直接接将序列内的值和修改操作的值进行调换,这样能保证多次修改,进行(修改->还原->修改(cdots))的操作。
此时,我们仍要修改排序。
- 考虑普通莫队的排序:先比较(a.l,b.l),再比较(a.r,b.r).
- 那么:对于添加了一维的带修莫队,就可以写成:
- 先比较(a.l,b.l),再比较(a.r,b.r),最后比较(a.t,b.t).
inline bool operator < (const ask a,const ask b){return bel[a.l] == bel[b.l] ? bel[a.r] == bel[b.r] ? a.t<b.t : a.r<b.r : a.l < b.l;}
复杂度分析
等以后慢慢写……
所以代码:
#include <bits/stdc++.h>
#define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
using namespace std;
const int INF = 0x3f3f3f3f , N = 1.4e5+5 , M = 1e6+5;
typedef long long ll;
typedef unsigned long long ull;
inline ll read(){
ll ret = 0 ; char ch = ' ' , c = getchar();
while(!(c >= '0' && c <= '9')) ch = c , c = getchar();
while(c >= '0' && c <= '9')ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
return ch == '-' ? - ret : ret;
}
int n,m;
int a[N];
int pl=1,pr,pans,len,bel[N];
int buc[M],now;
int ans[N];
struct ask{int l,r,id,t;}q[N];int qcnt;
inline bool operator < (const ask a,const ask b){return bel[a.l] == bel[b.l] ? bel[a.r] == bel[b.r] ? a.t<b.t : a.r<b.r : a.l < b.l;}
struct mdf{int p,v;}mo[N];int mcnt;
inline void mod(int k,int id){
if(q[id].l <= mo[k].p && mo[k].p <= q[id].r)
pans += !buc[mo[k].v]++,
pans -= !--buc[a[mo[k].p]];
swap(mo[k].v,a[mo[k].p]);
}
inline void add(int k){pans += !buc[a[k]]++;}
inline void del(int k){pans -= !--buc[a[k]];}
inline int calc(int l,int r,int id,int t){
while(pl > l)add(--pl);
while(pr < r)add(++pr);
while(pl < l)del(pl++);
while(pr > r)del(pr--);
while(now < t)mod(++now,id);
while(now > t)mod(now--,id);
return pans;
}
signed main(){
n = read() , m = read();
len = pow(n,2.0/3);
for(int i = 1 ; i <= n ; i ++)
a[i] = read(),
bel[i] = i / len + 1;
for(int i = 1 ; i <= m ; i ++){
char ch[2];int x,y;
scanf(" %s %d %d",ch,&x,&y);
if(ch[0] == 'Q')q[++qcnt] = (ask){x,y,qcnt,mcnt};
else mo[++mcnt] = (mdf){x,y};
}
sort(q+1,q+qcnt+1);
for(int i = 1 ; i <= qcnt ; i ++)
ans[q[i].id] = calc(q[i].l,q[i].r,i,q[i].t);
for(int i = 1 ; i <= qcnt ; i ++)
printf("%d
",ans[i]);
return 0;
}
剩下的优化还不会,等以后再来更新吧……
例题
P2709 小B的询问【板子中的板子】
P1972 [SDOI2009] HH的项链【玄学优化】