莫队+带修莫队 及优化详解

莫队

莫队算法(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++).
  • 对于移动右指针((pr o r)
    • (pr < r),与上①相同
      • add(++pr).
    • (pr > r),与上②相同
      • del(pr--).
  • 关于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的项链【玄学优化】

原文地址:https://www.cnblogs.com/Shinomiya/p/14906834.html