莫队

前言

这是一篇 LaTeX\LaTeX PPT 改 md。

今天的题单:点我

今天是我第一次上台,PPT 可能不是那么美观, 如果有错误请大胆指出。

例题不会很难,请放心食用。

简介

莫队简介

什么是莫队?

莫队是由莫涛大神提出的一种暴力区间操作算法,它框架简单、板子好写、复杂度优秀。

然而由于莫队算法应用的毒瘤,很多莫队题难度评级都很高(蓝紫黑),令很多初学者望而却步。但如果你真正理解了莫队的算法原理,它用起来还是挺简单的。

前置知识:

  • 分块思想。
  • sort 的用法(包括重载运算符或 cmp 函数,多关键字排序)。

使用莫队的情境

m=O(n)m=O(n)(即 mmnn 同阶),且 [l,r][l,r] 的答案能 O(1)O(1) 地转换到 [l1,r],[l,r+1],[l+1,r],[l,r1][l-1,r],[l,r+1],[l+1,r],[l,r-1] 区间(即相邻区间)的答案,那么莫队可以在 O(nn)O(n\sqrt{n}) 的时间复杂度内离线求出所有询问的答案。

注意:莫队是离线算法。如果题目强制在线,则不以可用莫队。

什么是离线、在线?

  • 如果算法需要知道所有询问才能开始算法,则称此算法为离线算法。
  • 读入一个询问,回答一个询问的算法叫在线算法。
  • 强制在线就是要求你读入一个询问就立马回答。

莫队的基本思想

离线存下所有询问,借助分块按照一定的顺序处理这些询问,使得询问之间可以互相利用(一般情况下为了方便,只会是本次询问利用上次询问的答案),以减小时间复杂度。

普通莫队

算法基础

算法流程

  1. 离线存下所有询问。
  2. 以二元组 (bel[li],ri)(bel[l_i],r_i) 为关键字升序对所有询问排序。
    其中 ii 表示当前询问编号,bel[li]bel[l_i](belong,属于)表示 lil_i 所在的块的编号。
  3. 遍历每个询问,维护两个指针 [l,r][l,r] 表示当前区间,nownow 表示当前答案。
    初始时 l=1,r=0l=1,r=0(如果 l=0l=0,那么我们还需要删除 a0a_0,导致一些奇怪的错误)。
    l,rl,r 需区别于 li,ril_i,r_i,它们一对是我们维护的指针(下标),一对是数据给出的询问。
  4. 移动区间 [l,r][li,ri][l,r]\to [l_i,r_i]。途中维护区间 [l,r][l,r] 的答案 nownow
  5. 移动结束后,记录区间 [li,ri][l_i,r_i] 的答案 ansians_i。(ansinowans_i\gets now)。

算法代码:洛谷 P3901 数列找不同(模板题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
constexpr int N=114514;
int n,m,a[N],S;// S:块长
// [1,S] 区间的块编号为 1,[S+1,2S] 区间的块编号为 2,以此类推。
inline int bel(int x){return (x-1)/S+1;}
struct query// 询问结构体
{
int l,r,id;// 分别为每个询问区间的左端点、右端点、询问的编号。
friend inline bool operator < (query x,query y)
{return (bel(x.l)==bel(y.l)?x.r<y.r:bel(x.l)<bel(y.l));}
};
query q[N];// 查询数组
bitset<N> ans;// 答案数组
// cnt[i]:i 这个数在当前区间 [l,r] 出现次数,cf:重复出现的数的数量。
// 如果 cf=0,[l,r] 中没有重复出现的数。
int cnt[N],cf=0;
// 移动区间
inline void add(int pos)// 添加 a[pos]
{
cnt[a[pos]]++;// 将 a[pos] 的出现次数 +1。
if(cnt[a[pos]]==2)cf++;// 如果已经出现两次,则重复了,cf++。
}
inline void del(int pos)// 删除 a[pos]
{
cnt[a[pos]]--;// 将 a[pos] 的出现次数 -1。
if(cnt[a[pos]]==1)cf--;// 如果当前只出现一次,则之前一定重复(出现两次),
// 而现在不重复了,cf--。
}

void mt()
{
S=int(ceil(pow(n,0.5)));// S=sqrt(n),根号分块
sort(q+1,q+m+1);// 结构体排序
for(int i=1,l=1,r=0;i<=m;i++)// 遍历每个询问
{
#define q q[i]// 偷懒
while(q.l<l)add(--l);// 向左扩展 l-1
while(r<q.r)add(++r);// 向右扩展 r+1
while(l<q.l)del(l++);// 向右删除 l
while(q.r<r)del(r--);// 向左删除 r
// 注意上面四句的顺序,需要先扩展在删除。
// 同时注意自减自加运算符是前置还是后置。
ans[q.id]=!cf;// 记录当前答案
#undef q
}
}

int main()
{
// input
mt();// 莫队
for(int i=1;i<=m;i++)puts(ans[i]?"Yes":"No");// 输出
return 0;
}

算法复杂度

下面的讨论中 m=O(n)m=O(n)

单次移动 l,rl,r 中的一个复杂度显然 O(1)O(1)

考虑 l,rl,r 分别移动的次数。

  • 考虑 ll:设块 ii 内的询问的左端点个数为 xix_i,则块 ii 中移动 ll 的次数顶多 xi×nx_i\times\sqrt{n}。一共 n\sqrt{n} 个块,移动 ll 的总时间复杂度为:

i=1n(xi×n)=(n)×i=1n(xi)=n×m=O(nn)\begin{aligned} &\sum_{i=1}^{\sqrt{n}} \left(x_i\times\sqrt{n}\right)\\ =&\left(\sqrt{n}\right)\times\sum_{i=1}^{\sqrt{n}} \left(x_i\right)\\ =&\sqrt{n}\times m\\ =&O(n\sqrt{n}) \end{aligned}

  • 考虑 rr:每块内的 xix_ilj(1jxi)l_j(1\le j\le x_i) 肯定一一对应着 xix_irjr_j
    显然这 xix_irjr_j 最多会使 rr 移动 nn 的长度(ljl_j 同一块时,按 rjr_j 升序,故不降)。
    一共 n\sqrt{n} 个块,移动 rr 的总时间复杂度为:n×n=O(nn)\sqrt{n}\times n=O(n\sqrt{n})
  • 则总时间复杂度为 O(1)×[O(nn)+O(nn)]=O(nn)O(1)\times [O(n\sqrt{n})+O(n\sqrt{n})]=O(n\sqrt{n})

算法优化

奇偶性排序

刚才的复杂度分析中提出了一些极端情况:xix_irjr_j 最多使 rr 移动 nn 的长度。

n\sqrt{n} 个块都可能使 rr 移动 nn 的长度,例如下列询问(n=100,S=10n=100,S=10):

1
2
3
4
1 1
10 100
11 1
20 100

按原先的排序策略,rr 需要反复横跳,十分浪费时间。

如果将处理顺序改为以下顺序将大大加速:

1
2
3
4
1 1
10 100
20 100
11 1

怎么修改排序策略?

依然以 bel[li]bel[l_i] 为第一关键字升序排序,若 bel[li]bel[l_i] 为奇数,以 rir_i 为第二关键字升序排序,反之若 bel[li]bel[l_i] 为偶数,以 rir_i 为第二关键字降序排序。

这样我们的 rr 指针在处理完这个奇数块的问题后,将在返回的途中处理偶数块的问题,再向 nn 移动处理下一个奇数块的问题,优化了 rr 指针的移动次数,一般情况下,这种优化能让程序快 30%30\% 左右。

——OI-Wiki。

实测:810810 ms \to 622622 ms,快约 23.2%23.2\%

奇偶性排序:代码

1
2
3
4
5
6
7
8
9
10
struct query
{
int l,r,id;
friend inline bool operator < (query x,query y)
{
if(bel(x.l)!=bel(y.l))return bel(x.l)<bel(y.l);
if(bel(x.l)&1)return x.r<y.r;
else return x.r>y.r;
}
};

坑点:注意重载运算符不能出现 a<b,a>ba<b,a>b 同时为真的情况,否则会 RE。如以下实现方式就是错误的:

1
2
3
4
5
6
7
8
9
struct query
{
int l,r,id;
friend inline bool operator < (query x,query y)
{
if(bel(x.l)!=bel(y.l))return bel(x.l)<bel(y.l);
return (bel(x.l)&1)==(x.r<y.r);// 这里会出错
}
};

常数级展开

发现 add()del() 两个函数可以压行并展开到 mt() 中。

这看似鸡肋的优化实测 572572 ms——又优化了 5050 ms。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void mt()
{
S=int(ceil(pow(n,0.5)));
sort(q+1,q+m+1);
for(int i=1,l=1,r=0;i<=m;i++)
{
#define q q[i]
// 压行并展开:
while(q.l<l)cf+=(++cnt[a[--l]]==2);// 与 add(--l) 等价
while(r<q.r)cf+=(++cnt[a[++r]]==2);// 与 add(++r) 等价
while(l<q.l)cf-=(--cnt[a[l++]]==1);// 与 del(l++) 等价
while(q.r<r)cf-=(--cnt[a[r--]]==1);// 与 del(r--) 等价
ans[q.id]=!cf;
#undef q
}
}

玄学剪枝

我考虑到有时候可能转移大半天还不如暴力重新算,所以想出了一个玄学剪枝:

1
2
3
4
5
6
// 如果转移代价大于重新算的代价
if(abs(q.l-l)+abs(q.r-r)>(r-l+1)+(q.r-q.l+1))
{
while(l<=r)cf-=(--cnt[a[r--]]==1);// 清除
r=(l=q.l)-1;// 直接跳转
}

(这段语句加在 #define q q[i] 后面。)

没想到只优化了 11 ms(悲。也许是每次都判断的代价太大,抵消了直接跳转的优化。

预处理 belbel

我写好几题才发现其他大佬都预处理了 belbel,于是赶紧改过来。快了 5858 ms

1
2
3
4
5
6
7
8
9
10
11
12
13
int S;int bel[N];
inline void initbel()// 1~S: 1
{
S=int(ceil(pow(n,0.5)));// S=sqrt(n)
for(int i=1;i<=n;i++)bel[i]=(i-1)/S+1;
}
...
void mt()
{
initbel();
sort(q+1,q+m+1);
...
}

例题

例题:DQUERY - D-query

简要题意:给出一个长度为 nn 的数列 aamm 个询问,每次询问给出数对 l,rl,r 表示查询区间 [l,r][l,r] 中有多少不同的数。

数据范围:n3×105,m2×105,ai106n\le 3\times10^5,m\le2\times10^5,a_i\le10^6

板子题,难度在于如何 O(1)O(1) 转移答案。

考虑用数组 cnticnt_i 表示 [l,r][l,r]ii 出现了几次,变量 nownow 表示 [l,r][l,r] 中有多少不同的数。

要添加 aposa_{pos},那么 cnt[apos]cnt[apos]+1cnt[a_{pos}]\gets cnt[a_{pos}]+1

此时若 cnt[apos]=1cnt[a_{pos}]=1,即多了一个不同的数,那么 nownow+1now\gets now+1

同理删除 aposa_{pos}cnt[apos]cnt[apos]1cnt[a_{pos}]\gets cnt[a_{pos}]-1,若 cnt[apos]=0cnt[a_{pos}]=0(少了一个数),nownow1now\gets now-1

其他的正常地跑莫队即可。但此题似乎卡常。

例题:P2709 小B的询问

简要题意:给出一个长度为 nn 的数列 aa(值域 [1,k][1,k]),mm 个询问,每次询问给出数对 l,rl,r 表示查询:

i=1kci2\sum\limits_{i=1}^k c_i^2

其中 cic_i 表示数字 ii[l,r][l,r] 中的出现次数。

数据范围:1n,m,k5×1041\le n,m,k \le 5\times 10^4

难度依然在于如何 O(1)O(1) 转移答案。

cc 数组很好维护,但答案(设它为 ss)就不那么好维护了。

由于每次添加或删除数时只会改变 ca[pos]c_{a[pos]},而且只会 ±1\pm1。所以:

s=i=1kci2=c12++ca[pos]2++ck2s=\sum\limits_{i=1}^k c_i^2=c_1^2+\cdots+c_{a[pos]}^2+\cdots+c_k^2

可得

s=c12++(ca[pos]±1)2++ck2=c12++ca[pos]2±2×ca[pos]+1++ck2=s±2×ca[pos]+1\begin{aligned} s'&=c_1^2+\cdots+(c_{a[pos]}\pm 1)^2+\cdots+c_k^2\\ &=c_1^2+\cdots+c_{a[pos]}^2\pm2\times c_{a[pos]}+1+\cdots+c_k^2\\ &=s\pm2\times c_{a[pos]}+1 \end{aligned}

转移时修改即可。

例题:P5268 [SNOI2017] 一个简单的询问

简要题意:给出一个长度为 nn 的数列 aa(值域 [1,n][1,n]),mm 个询问,每次询问给出数对 l1,r1,l2,r2l_1,r_1,l_2,r_2 表示查询:

x=0get(l1,r1,x)×get(l2,r2,x)\sum\limits_{x=0}^\infty \text{get}(l_1,r_1,x)\times \text{get}(l_2,r_2,x)

get(l,r,x)\text{get}(l,r,x) 表示区间 [l,r][l,r]xx 的出现次数。

数据范围:n,m5×104,1ainn,m\le 5\times 10^4,1\le a_i\le n

首先因为每次询问给出的是一个四元组,所以莫队不能直接做。

考虑对询问进行拆分。

可以发现,get(l,r,x)=get(1,r,x)get(1,l1,x)\text{get}(l,r,x)=\text{get}(1,r,x)-\text{get}(1,l-1,x)。展开:
(为方便,记 g(p)=get(1,p,x)\text{g}(p)=\text{get}(1,p,x)

x=0get(l1,r1,x)×get(l2,r2,x)=x=0(g(r1)g(l11))×(g(r2)g(l21))=x=0g(r1)×g(r2)g(l11)×g(r2)g(l21)×g(r1)+g(l11)×g(l21)\begin{aligned} &\sum\limits_{x=0}^\infty \text{get}(l_1,r_1,x)\times\text{get}(l_2,r_2,x)\\ =&\sum\limits_{x=0}^\infty \big(\text{g}(r_1)-\text{g}(l_1-1)\big)\times \big(\text{g}(r_2)-\text{g}(l_2-1)\big)\\ =&\sum\limits_{x=0}^\infty \text{g}(r_1)\times\text{g}(r_2)-\text{g}(l_1-1)\times\text{g}(r_2)\\ &-\text{g}(l_2-1)\times\text{g}(r_1)+\text{g}(l_1-1)\times\text{g}(l_2-1) \end{aligned}

若记 ask(l,r)=g(l)×g(r)\text{ask}(l,r)=\text{g}(l)\times\text{g}(r)

原式=ask(r1,r2)ask(l11,r2)ask(l21,r1)+ask(l1,l2)\text{原式}=\text{ask}(r_1,r_2)-\text{ask}(l_1-1,r_2)-\text{ask}(l_2-1,r_1)+\text{ask}(l_1,l_2)

肉眼可见,我们将原来的四元询问转换为四个二元询问。虽然 l,rl,r 并不表示区间,但我们可以用相同的思想处理询问。

如何 O(1)O(1) 转移 ask(l,r)\text{ask}(l,r)
考虑若其中一个(g(l),g(r)\text{g}(l),\text{g}(r))增加 1,则它们的积会增加另一个数。具体见代码。

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// cl[i]: [1,l] 中 i 的个数;cr[i]: [1,r] 中 i 的个数;now: sum(cl[i]*cr[i])
int cl[N],cr[N],now,ans[N];
void mt()
{
init();
sort(q+1,q+(m<<2)+1);// 原来的一个询问拆成四个,所以 <<2
for(int i=1,l=0,r=0;i<=(m<<2);i++)
{
#define q q[i]
while(l<q.l)++cl[a[++l]],now+=cr[a[l]];// 相应的转移
while(r<q.r)++cr[a[++r]],now+=cl[a[r]];
while(q.l<l)--cl[a[l]],now-=cr[a[l--]];
while(q.r<r)--cr[a[r]],now-=cl[a[r--]];
ans[q.id]+=(q.t*now);
#undef q
}
}

int main()
{
...
for(int i=1,l1,r1,l2,r2,p=1;i<=m;i++)
{
scanf("%d %d %d %d",&l1,&r1,&l2,&r2);
q[p++]={min(l1-1,l2-1),max(l1-1,l2-1),i,1 };// 拆开
q[p++]={min(l1-1,r2), max(l1-1,r2), i,-1};
q[p++]={min(l2-1,r1), max(l2-1,r1), i,-1};
q[p++]={min(r1,r2), max(r1,r2), i,1 };
}
...
return 0;
}

卡常:P1972 [SDOI2009] HH的项链

题意:同 DQUERY - D-query。但数据范围不同,还卡莫队。

我们就这么屈服了吗?没有!

其实本题莫队理论上 n=106,O(nn)109n=10^6,O(n\sqrt{n})\approx 10^9 是可以过的,因为莫队经常跑不满。但经不住毒瘤出题人卡莫队。

考虑常数优化。注意到大部分时间花在转移 l,rl,r 指针上。

由于 aia_i 的波动程度可能非常大,又由于我们的转移方式是

1
while(q.l<l)now+=(!(cnt[a[--l]]++));

访问 cnt 时下标波动程度大,导致访问慢。

如何加速?关键在于用等价但是数组访问只与下标有关的转移方式。因为下标经过排序后相对连续。

考虑记录两个数组 prei,nxtipre_i,nxt_i 分别表示上一个和下一个出现 aia_i 的位置的下标。如添加 l=il=i 时,r<nxtir<nxt_i,即右边那个最近的 aia_i 已经不在 [l,r][l,r] 中(显然左边那个更不会),那么 [l,r][l,r] 中多了一个不同的数,nownow+1now\gets now+1。其他同理。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
for(int i=1;i<=n;i++)pre[i]=lst[a[i]],lst[a[i]]=i;
fill(lst,lst+A,0x3f3f3f3f);
for(int i=n;i;i--)nxt[i]=lst[a[i]],lst[a[i]]=i;
for(int i=1,l=1,r=0;i<=m;i++)
{
#define q q[i]
while(q.l<l)now+=(nxt[--l]>r);
while(r<q.r)now+=(pre[++r]<l);
while(l<q.l)now-=(nxt[l++]>r);
while(q.r<r)now-=(pre[r--]<l);
ans[q.id]=now;
#undef q
}

*带修莫队

简介

如何实现带修莫队?

发现普通莫队不支持修改,那么如何使它支持修改操作呢?

考虑存询问时加一个变量 tit_i 表示进行此询问时前面修改了几次。同时存下每一个修改操作(无需排序)。

再新增一个指针 tt 表示当前区间所在的时间位置。那么移动方向就从 44 个变为 66 个:[l1,r,t],[l,r+1,t],[l+1,r,t],[l,r1,t],[l,r,t1],[l,r,t+1][l-1,r,t],[l,r+1,t],[l+1,r,t],[l,r-1,t],[l,r,t-1],[l,r,t+1]。新增的两个为时间轴上的移动。

例题

例题:P1903 [国家集训队] 数颜色 / 维护队列

简要题意:给出一个长度为 nn 的数列,mm 个操作,要求支持两种操作:查询区间有多少不同的数、单点修改。

数据范围:n,m1.33333×105,ai106n,m\le 1.33333\times 10^5,a_i\le 10^6

板子题,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
constexpr int N=214514,A=1145141;// A:a 的值域
int n,m,S,qm,a[N];// qm:询问的个数
inline int bel(int x){return (x-1)/S+1;}// 分块
struct query
{
int l,r,t,id;// 额外记录时间
friend inline bool operator < (query x,query y)
{// 若 l 所在块不同 按 l 的块的编号 否则 若 r 所在块不同 按 r 的块的编号 否则按时间排
return (bel(x.l)^bel(y.l)?bel(x.l)<bel(y.l):(bel(x.r)^bel(y.r)?bel(x.r)<bel(y.r):x.t<y.t));
}
};query q[N];
struct modify// 新增:修改操作
{int p,v;};modify mo[N];

int cnt[A],now,ans[N];// 类似于普通莫队
void mt()
{
S=int(ceil(pow(n,0.66)));// 这里块长需要调整,具体可以可以看
// https://oi-wiki.org/misc/modifiable-mo-algo/ 中的证明
sort(q+1,q+qm+1);
for(int i=1,l=1,r=0,t=0;i<=qm;i++)
{
#define q q[i]
#define p mo[t].p
#define v mo[t].v
while(q.l<l)now+=(!(cnt[a[--l]]++));// 类似
while(r<q.r)now+=(!(cnt[a[++r]]++));
while(l<q.l)now-=(!(--cnt[a[l++]]));
while(q.r<r)now-=(!(--cnt[a[r--]]));
// 压行:先加时间 如果在当前区间内 相应的转移 *直接交换 v 和 a[p]
while(t<q.t){t++;if(l<=p&&p<=r)now-=(!(--cnt[a[p]])-!(cnt[v]++));swap(a[p],v);}
while(q.t<t){if(l<=p&&p<=r)now-=(!(--cnt[a[p]])-!(cnt[v]++));swap(a[p],v);t--;}
ans[q.id]=now;
#undef q
#undef p
#undef v
}
}

*回滚莫队

简介

何时使用回滚莫队?

对于某些问题,普通莫队可能难以解决。原因在于删除和添加只有一个可实现。这时就需要回滚莫队了。

先看题:AT_joisc2014_c 歴史の研究

简要题意:区间询问,给出一个长度为 nn 的数列 aamm 个询问,每次询问要求回答

maxx{x×cnt(x)}\max_{\forall x}\{x\times\operatorname{cnt}(x)\}

其中 cnt(x)\operatorname{cnt}(x),表示 xx[l,r][l,r] 区间中的出现次数。

此题添加数很方便,但删除数却很麻烦。因为当最大值改变(如变小)时,我们无法确定新的最大值。

又如求区间 mex\operatorname{mex} 时(P4137 Rmq Problem / mex),删除数方便,添加数麻烦。

这时就要用到回滚莫队了。它只用删除和添加中的一个操作,剩下的就回滚解决。

算法实现

算法流程

  1. 对原序列进行分块,对询问按以 bel[li]bel[l_i] 升序为第一关键字,rir_i 升序为第二关键字排序。
  2. 如果 bel[li]bel[li1]bel[l_i]\ne bel[l_{i-1}],那么将 ll 初始化为 bel[li]bel[l_i] 的右端点的后一个位置,将 rr 初始化为 bel[li]bel[l_i] 的右端点。
    此时:
    1. bel[li]=bel[ri]bel[l_i]=bel[r_i],直接暴力回答。
    2. 反之,
      1. 移动 rrir\to r_i
      2. 暂存当前答案 tmptmp
      3. 移动 llil\to l_i
      4. 记录答案 ansinowans_i\gets now
      5. 回滚,使 ll 回滚回 bel[li]bel[l_i] 的右端点的后一个位置,同时更新答案 nowtmpnow\gets tmp

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
typedef long long ll;//!
constexpr int N=114514,INF=0x3f3f3f3f;
int n,m;
int a[N];
int bel[N],S;
struct query
{
int l,r,id;
friend inline bool operator < (query x,query y)
{return (bel[x.l]^bel[y.l]?bel[x.l]<bel[y.l]:x.r<y.r);}
};
query q[N];
ll ans[N],now,tmp;
unordered_map<int,int> cnt,tcnt;

void rbmt()
{
S=int(ceil(pow(n,.5)));
for(int i=1;i<=n;i++)bel[i]=(i-1)/S+1;
sort(q+1,q+m+1);bel[q[0].l=0]=INF;
for(int i=1,l,r;i<=m;i++)
{
if(bel[q[i].l]^bel[q[i-1].l])cnt.clear(),now=-INF,r=bel[q[i].l]*S,l=r+1;
#define q q[i]
if(bel[q.l]==bel[q.r])
{
tmp=-INF,tcnt.clear();
for(int j=q.l;j<=q.r;j++)tmp=max(tmp,1ll*(++tcnt[a[j]])*a[j]);
ans[q.id]=tmp;
}
else
{
while(r<q.r)now=max(now,1ll*(++cnt[a[++r]])*a[r]);
tmp=now;
while(q.l<l)now=max(now,1ll*(++cnt[a[--l]])*a[l]);
ans[q.id]=now;
now=tmp;// rollback
while(l<=bel[q.l]*S)--cnt[a[l++]];
}
#undef q
}
}

int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",a+i);
for(int i=1;i<=m;i++)scanf("%d %d",&q[i].l,&q[i].r),q[i].id=i;
rbmt();
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
return 0;
}

莫队延迟更新答案

出处

该方法整理自:

也许还有。我看到的就这些。

其实就是普通莫队的升级版。

算法思想 & 实现

对莫队算法进行观察,不难发现移动 l,rl,r 指针过程中的答案我们并不需要,于是就不一定要 O(1)O(1) 移动指针并更新答案。只需要能 O(1)O(1) 移动指针,并记录修改,移动 l,rl,r 结束后再 O(n)\le O(\sqrt{n}) 地更新答案即可。

这个寄巧很早就出现了,但无人详细记载,故记之。

更具体的,考虑 P4137 Rmq Problem / mex 这题。

此题 n,m,ai2×105n,m,a_i\le 2\times 10^5,我们可以进行值域分块,开个桶,cnticnt_i 表示 ii[l,r][l,r] 中出现次数(而非是否出现,方便维护)。每个块 SS 维护块内的数在 [l,r][l,r] 中出现的个数,即

exiS=iS[cnti>0]exi_S=\sum_{i\in S}[cnt_i>0]

当移动指针,添加/删除数时,直接修改 cntcnt,同时修改 exiexi。不急着更新答案,但移动指针结束后,扫过值域分块,O(n)O(\sqrt{n}) 更新答案 nownow。具体的,O(n)O(\sqrt{n}) 遍历每个块 SS,若 exiS=Sexi_S=|S|,跳到下一个块,反之 mex\operatorname{mex} 必定在块内,再次 O(n)O(\sqrt{n}) 遍历每个块内元素 iSi\in S,必定 i,cnti=0,j<i,cnti>0\exist i,cnt_i=0,\forall j<i,cnt_i>0,此时 ii 即为所求。

本质上,该方法就是利用莫队对答案修改 | 询问次数 O(nn)O(n)O(n\sqrt{n})|O(n) 的特性,调整对答案的维护复杂度,将原来维护的答案修改 | 询问复杂度 O(1)O(1)O(1)|O(1) 的高要求降为 O(1)O(n)O(1)|O(\sqrt{n}),以适应更多题目。

但有时第一时间想到的是 O(n)O(1)O(\sqrt{n})|O(1) 做法,此时就需要修改策略。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
constexpr int N=214514,B=514;
int n,m,a[N];
struct qry{int l,r,id;};
qry q[N];

int S,bel[N];
int cnt[N],exi[B];
int ans[N];
void mt()
{
S=int(ceil(n*pow(m,-.5)));
for(int i=0;i<=n;i++)bel[i]=i/S+1;
std::sort(q+1,q+m+1,[](qry x,qry y)->bool
{
return bel[x.l]!=bel[y.l]?
(bel[x.l]<bel[y.l]):
(bel[x.l]&1)==(x.r<y.r);
});
for(int i=1,l=1,r=0,j,k;i<=m;i++)
{
#define q q[i]
while(q.l<l)l--,exi[bel[a[l]]]+=!(cnt[a[l]]++);
while(r<q.r)r++,exi[bel[a[r]]]+=!(cnt[a[r]]++);
while(l<q.l)exi[bel[a[l]]]-=!(--cnt[a[l]]),l++;
while(q.r<r)exi[bel[a[r]]]-=!(--cnt[a[r]]),r--;
for(j=1;exi[j]==S;j++);
for(k=(j-1)*S;cnt[k];k++);
ans[q.id]=k;
}
}

int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",a+i);
for(int i=1;i<=m;i++)scanf("%d %d",&q.l,&q.r),q.id=i;
mt();
for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
return 0;
}

例题:P5906 【模板】回滚莫队&不删除莫队

对不住了,回滚莫队。

题意:给定一个序列,多次询问一段区间 [l,r][l,r],求区间中相同的数的最远间隔距离。序列中两个元素的间隔距离指的是两个元素下标差的绝对值。

数据范围:1n,m21051\leq n,m\leq 2\cdot 10^51ai21091\leq a_i\leq 2\cdot 10^9

考虑普通莫队。硬伤:当删除值时,若最大值改动,难以维护。

考虑离散化后对原题值域进行分块。维护每个值在 [l,r][l,r] 中的最远距离,同时维护块内距离最大值和全局最大值。此时复杂度(修改 | 询问)O(n)O(1)O(\sqrt{n})-O(1)

考虑转换成 O(n)O(1)O(\sqrt{n})-O(1)

「qwaszx」大佬的神之一手——再次值域分块!

刚才的对数进行值域分块就不要了,只要保存每个数在区间中最边缘的两个位置,同时分块改为对距离值域分块!具体的,cnticnt_i 表示有多少个数满足 [l,r][l,r] 内最远间隔距离为 iisumSsum_S 表示块 SScntcnt 之和,即 sumS=jScntjsum_S=\sum_{j\in S}cnt_j

若修改最远间隔距离,直接 O(1)O(1) 修改 cntcntsumsum,而查询时则从大往小扫过值域分块,具体的,块 TT 为最大的一个块使得满足 sumT>0sum_T>0,则一定 kS,l,l>k,cntl=0,cntk>0\exist k\in S,\forall l,l>k,cnt_l=0,cnt_k>0kk 本身为最远间隔距离的最大值

实现与 P4137 Rmq Problem / mex 类似。

例题:P3834 【模板】可持久化线段树 2

嘿嘿没想到吧,主席树!

又双㕛叒叕考虑值域分块。像上一题一样维护 cnt,sumcnt,sumO(1)O(1) 修改。cnticnt_i 表示 [l,r][l,r]ii 的数量。

从小到大扫过每个块,记录 sumsum 的前缀和,若前缀和 preS1+sumSkpre_{S-1}+sum_S\ge k,则第 kk 大在该区间内。从小到大扫过 SS 内表示的每个数,累加 cntcnt 直到找到第 kk 大。

结束

Thanks!\Huge \text{Thanks!}


后记

此 PPT 是本人一边学习莫队一边写的,肯定有诸多不足,还请包容。

当然还有树上莫队、二维莫队,由于难度过高,我自己也不会,就不多做介绍了。可以通过 OI-Wiki 自学。