Loading

莫队

普通莫队

莫队通过将所有询问按某种顺序排序,并暴力地增、删元素来在询问间转移。

莫队的适用条件是,信息不能高效地合并,但可以高效地加入、删除。具体地,若加入、删除一个元素的复杂度都为 \(\mathcal{O}(a)\),那么莫队的复杂度是 \(\mathcal{O}(na\sqrt{m})\)

复杂度分析

我们将序列分成大小约为 \(S\) 的若干段,段个数约为 \(\frac{n}{S}\)。对于所有询问,以左端点所在块为第一关键字,右端点为第二关键字排序。考虑在同一个块内,指针移动的复杂度:左端点会贡献 \(\mathcal{O}(xS)\),其中 \(x\) 是左端点在这个块内的询问个数;右端点会贡献 \(\mathcal{O}(n)\)。所以总复杂度是 \(\mathcal{O}(mS+\frac{n^2}{S})\),当 \(S=\frac{n}{\sqrt{m}}\) 时最优,为 \(\mathcal{O}(n\sqrt{m})\) 次指针移动。

常数优化

对于左端点在奇数块的询问,将其按照右端点从小到大排序;左端点在偶数块的询问,右端点从大到小排序。

[AHOI2013] 作业

给定整数序列 \(\{a_n\}\)\(m\) 次查询区间 \([l,r]\) 内值在 \([a,b]\) 中的不同数的个数。

\(1\le n\le 10^5,1\le m\le 10^5\)

如果对序列维做莫队,用树状数组维护每种数是否出现,这是 \(\mathcal{O}(n\sqrt{m}\log n)\) 的。但我们发现它有 \(\mathcal{O}(n\sqrt{m})\) 次修改和 \(\mathcal{O}(m)\) 次询问,若用分块将修改和询问的复杂度分别弄成 \(\mathcal{O}(1)\)\(\mathcal{O}(\sqrt{n})\),就做完了。

值域分块,它需要支持 \(\mathcal{O}(1)\) 单点修改,\(\mathcal{O}(\sqrt{n})\) 区间求和,直接做就行了。

总复杂度 \(\mathcal{O}(n\sqrt{m}+m\sqrt{n})\)

[Ynoi2016] 这是我自己的发明

给定一棵有根树,每个点有颜色 \(a_i\),开始时根是 \(1\)。有 \(m\) 次操作,每次是以下两种之一:

  • 给定 \(x\),将根更改为 \(x\)
  • 给定 \(x,y\),询问有多少个二元组 \((u,v)\) 满足 \(u\)\(x\) 的子树内,\(v\)\(y\) 的子树内,且 \(a_u=a_v\)

我们以 \(1\) 为根求出每个点的 dfs 序。不管根是哪个点,一个点的子树对应在 dfn 上一定是 \(\mathcal{O}(1)\) 个区间。有三种可能:

  • 根在 \(u\) 的某个儿子的子树内;
  • 根是点 \(u\)
  • 根不在点 \(u\) 的子树内。

所以每个询问都可以拆成 \(\mathcal{O}(1)\) 个形如“给定 \(l_1,r_1,l_2,r_2\),求 \([l_1,r_1]\)\([l_2,r_2]\) 中有多少颜色相同的点对”的询问。而这样的询问是可差分的。莫队即可。

坑点:应该将点的颜色映射在 dfn 上。

代码:https://github.com/YOYO-UIAT/oi-code/blob/main/luogu/P4689.cpp

JOISC 2014 历史研究

给定整数序列 \(\{a_n\}\),定义 \(c_{l,r}(x)\)\(x\)\(a_{l\dots r}\) 中的出现次数,\(m\) 次询问,每次给定区间 \([l,r]\),求 \(\max_x\{x\cdot c_{l,r}(x)\}\)

\(1\le n,m\le 10^5\)

不用回滚莫队。可以发现答案只有 \(\mathcal{O}(n)\) 种,将这些可能的答案放在一起离散化,然后值域分块即可。复杂度 \(\mathcal{O}(n\sqrt{m}+m\sqrt{n})\)

代码:https://github.com/YOYO-UIAT/oi-code/blob/main/luogu/AT1219.cpp

[Ynoi2015] 盼君勿忘

给定整数序列 \(\{a_n\}\)\(m\) 次询问,每次给定区间 \([l,r]\) 和模数 \(p\),求区间中所有子序列去重后的和 \(\bmod p\)

\(1\le n,m,a_i\le 10^5\)

设询问的区间长度为 \(L\),对于出现了 \(c\) 次的数 \(x\),它对答案的贡献是 \(x(2^L-2^{L-c})\)\(x2^L\) 的维护是很平凡的,只需维护区间中出现过的数之和。考虑 \(x2^{L-c}\) 如何维护:可以发现区间中不同的出现次数只有 \(\mathcal{O}(\sqrt{n})\) 种(这是常见套路),所以维护每一种出现次数的 \(x\) 之和,回答询问时光速幂即可。

P3604 美好的每一天

给定一个由小写字母组成的字符串 \(S\)\(m\) 次询问某个区间内有多少子区间可以重排成一个回文串。

\(1\le \lvert S\rvert,m\le 6\times 10^4\),3s。

首先,一个字符串 \(T\) 可以重排成回文串,当且仅当其中有至多一种字母出现了奇数次。我们将 \(\mathtt{a}\sim\mathtt{z}\) 状压成 \(2^0,2^1,\dots,2^{25}\),那么就是询问一个区间内有多少子区间满足它的异或和的 \(\operatorname{popcount}\le 1\)。又因为区间异或和可以用前缀异或和表示,所以就做完了。

高维莫队

我不会证的结论:对于 \(k\) 维莫队,假如有 \(m\) 个询问,且每一维的大小限制分别为 \(a_1,a_2,\dots,a_k\),那么将前 \(k-1\) 维的分块大小都设成

\[\sqrt[k]{\frac{\prod_{i=1}^k a_i}{m}} \]

是最优的,时间复杂度是

\[\mathcal{O}\left(\sqrt[k]{m^{k-1}\prod_{i=1}^k a_i}\right). \]

二次离线莫队

对于一些问题,普通莫队的做法是维护一个变量表示当前的答案,然后用数据结构维护一些信息来更新这个答案,这会在数据结构上产生 \(\mathcal{O}(n\sqrt{m})\) 次询问和 \(\mathcal{O}(n\sqrt{m})\) 次修改。若询问可差分,我们可以将在数据结构上的询问再次离线,然后做扫描线。

P5047 [Ynoi2019 模拟赛] Yuno loves sqrt technology II

给定整数序列 \(\{a_n\}\)\(m\) 次查询一个区间的逆序对个数。

\(1\le n,m\le 10^5\),250ms,32MB。

\(F(l,r,p)=\sum_{l\le i\le r} [a_i<a_p],G(l,r,p)=\sum_{l\le i\le r} [a_i>a_p]\),设当前答案为 \(\mathrm{ans}\)。在莫队过程中:

  • \(l\) 左移:\(l\gets l-1,\mathrm{ans}\gets \mathrm{ans}+F(l+1,r,l)\)
  • \(r\) 右移:\(r\gets r+1,\mathrm{ans}\gets \mathrm{ans}+G(l,r-1,r)\)
  • \(l\) 右移或 \(r\) 左移:同理。

显然 \(F\)\(G\) 都是可差分的,所以关于 \(\mathrm{ans}\) 的更改可以分为:(1) \(F(1,x,x)\)\(G(1,x,x)\);(2) \(F(1,x,y)\)\(G(1,x,y)\)

对于第一种,树状数组与处理即可;对于第二种,将所有这样的询问做扫描线,用 \(\mathcal{O}(\sqrt{n})\) 单点修改,\(\mathcal{O}(1)\) 区间求和的分块即可。

这样时间复杂度可以做到 \(\mathcal{O}(n(\sqrt{n}+\sqrt{m}))\),但空间也带根号,会被卡空间。但考虑到每次移动指针都是一段连续的询问,所以只需要存 \(\mathcal{O}(m)\) 个询问即可。具体细节参见代码。

代码:https://github.com/YOYO-UIAT/oi-code/blob/main/luogu/P5047.cpp

树上括号序莫队

用于处理查询树上一段链的信息,可以做到与普通莫队相同的时间复杂度。

将树映射到括号序上,每个点在括号序中恰好出现两次,位置设为 \(l_u\)\(r_u\)。设当前查询的是 \(x\to y\) 的链上的信息:

  • \(\operatorname{lca}(x,y)=x\),则用莫队计算括号序中在 \([l_x,l_y]\) 中恰好出现过一次的点的信息;
  • 否则,计算 \([r_x,l_y]\) 中恰好出现过一次的点的信息,并将 \(\operatorname{lca}\) 的信息额外加上。

带修莫队

加一维时间维,和三维莫队本质相同。假如有 \(c\) 个修改和 \(q\) 个询问,那么分块的大小是 \(\sqrt[3]{\frac{n^2c}{q}}\),时间复杂度是 \(\mathcal{O}(\sqrt[3]{n^2cq^2})\)。当然块长一般都会直接设成 \(n^{2/3}\)

WC2013 糖果公园

给定一棵 \(n\) 个点的树,\(m\) 次操作,每次是单点修改或者查询一段链上的简单莫队信息。

\(1\le n,m\le 10^5\),6s。

直接用树上带修莫队做就行了。注意若修改的次数是 \(0\),块长要特判。

代码:https://github.com/YOYO-UIAT/oi-code/blob/main/luogu/P4074.cpp

回滚莫队

假如只能支持快速加入或者删除,并且支持快速撤销,仍然可以做到 \(\mathcal{O}(n\sqrt{m})\) 的时间复杂度。

P5906 不删除莫队

给定整数数列 \(\{a_n\}\),多次询问某个区间 \([l,r]\) 内的 \(\max_{l\le i<j\le r\land a_i=a_j} j-i\),若不存在则输出 \(0\)

\(1\le n,m\le 2\times 10^5\)

将序列分成 \(\frac{n}{\sqrt{m}}\) 块,按照左端点所在块的编号为第一关键字、右端点为第二关键字,将所有询问排序。对于每一个询问:

  1. 若当前左端点与上次的左端点不在同一个块内,撤销所有操作,并将莫队的指针放在当前块的右端点;
  2. 若左端点与右端点在同一个块内,暴力求解,转 \(1\)
  3. 移动莫队的右指针;
  4. 移动莫队的左指针,然后计算答案;
  5. 撤销 \(4\) 中的操作。

可以理解为,对于每一个块,左端点在这个块内的询问,右端点是单调递增的,所以在右端点处只会加不会删;对于左端点,它移动的距离每次不超过块大小,所以每次计算完暴力撤销复杂度就是对的。

代码:https://github.com/YOYO-UIAT/oi-code/blob/main/luogu/P5906.cpp

不添加莫队:P4137 Rmq Problem / mex

多次询问某个区间的 \(\operatorname{mex}\)

\(1\le n,m\le 2\times 10^5\)

与不删除莫队相似,我们先将询问排序,但这次对于右端点要降序排序。对于每一个询问:

  1. 若当前左端点与上次的左端点不在同一个块内,撤销直到莫队的右指针到达 \(n\),然后删除直到莫队的左指针到达当前块的左端点;
  2. 不需要暴力求解
  3. 移动莫队的右指针;
  4. 移动莫队的左指针,然后计算答案;
  5. 撤销 \(4\) 中的操作。

代码:https://github.com/YOYO-UIAT/oi-code/blob/main/luogu/P4137.cpp

PS:这题有非常简单的 \(1\log\) 做法。

posted @ 2022-02-14 20:26  Alan_Zhao_2007  阅读(313)  评论(2编辑  收藏  举报