莫队+带修莫队 及优化详解
莫队
莫队算法(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++)
.
- ①若\(pl > l\),表示当前区间左端点短所求,则一步步向左移动左端点。
- 对于移动右指针(\(pr\to 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\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的项链【玄学优化】