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

莫队

莫队算法(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,M\le 10^5\)

(以下取\(N,M\)同阶)

扩展一下这个问题,变成"求\((L{i},R_i)\)区间内有多少对数字相同".

如果完完全全暴力的话,开类似于桶排序的桶,每次在\((L,R)\)的区间内更新桶,并计算答案,复杂度\(O(N^3)\).

莫队①优化一下这个过程:

  • 用两个指针\(pl\)\(pr\),两个指针所指的标号内是我们维护的区间。于是我们可以仅移动这两个指针来维护区间。
  • 每一步移动的复杂度是\(O(1)\).
  • \(pans\)表示这个区间内的答案

对于移动指针的过程,假设要将指针移动到\((l,r)\)

  • 对于移动左指针(\(pl\to 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\to 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\to N\space ,\space pr:1\to N\)\(pl:N\to 1\space,\space pr:N\to1\)

这样会出现很多很多次无用的扩展

考虑如何优化这些扩展:

莫队的思路是分块

取正整数\(B\),把序列每\(B\)个分为一段,即分段长度是\(B\).

对于左端点在同一块内的相邻询问,右端点递增,一共至多扩展\(N\) 次,左端点每次询问至多扩展\(B\) 步;对于左端点不在同一块内的相邻询问,至多有\(\frac nB\) 个。复杂度为\(O(N\frac NB + BN)\) 次扩展的复杂度。
利用均值不等式,\(N\frac NB+BN \ge2\sqrt{N\frac NB*BN}=2N\sqrt N\),当且仅当 $B =\sqrt N $时等号成立。

故取\(B=\sqrt N\),最优复杂度为\(O(N\sqrt 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(N\sqrt 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\n",ans[i]?"Yes":"No");
	return 0;
}



基本的莫队已经结束了。我们来看一道例题:P1494 [国家集训队] 小Z的袜子

给定序列\(A_n\),每次询问查询\((L_i,R_i)\)区间内任选两个数,选到相同数字的概率。

\(n\le5\times 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\n",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\n",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\n",ans[i]);
	return 0;
}

剩下的优化还不会,等以后再来更新吧……


例题

P2709 小B的询问【板子中的板子】
P1972 [SDOI2009] HH的项链【玄学优化】

posted @ 2021-06-20 15:10  Last-Order  阅读(791)  评论(2编辑  收藏  举报