莫队初探

莫队说:

众所周知,"莫队算法"是用于一类离线区间询问问题的常用算法,以适用性广、代码量短、实际运行速度快、适合骗分 等优点著称,广泛在OI/ACM中被使用和研究。

莫队作为基于暴力的离线算法,降低时间复杂度的主要方法在于,对各询问作出合适的处理。简单说,通过对各讯问进行合理的排序,使得后面的询问可以充分利用之前询问所得到的信息,就可以神奇地将时间复杂度从 O(NM) 降至 O(NM)
这样看来的确适合骗分

普通莫队

来道例题:AcWing 2492. HH的项链
最简单的方法当然是来一个询问就对区间[L, R]进行扫描,期望时间复杂度 O(NM),不能再快了
怎么优化呢?我们可以很容易地发现,对于区间[L,R],我们可以将它的状态转移至[L+1,R][L1,R][L,R+1][L,R1]。因此,我们可以通过双指针的方法,将区间[Li,Ri]转移至[Li+1,Ri+1]。我们也可以很容易地发现,时间复杂度还是O(NM)并没什么用,但又有点用,因为这是莫队的基础。
再往后,我们可以发现,这种优化的瓶颈在于两个端点的移动次数。只要将右端点升序排序,就可以使右端点最多移动 N 次。这种方式给我们了启发,如果将相邻的左端点也排在一起,复杂度不就降下来了吗?
莫队维护左端点的方法是分块。令每个块的长度为 S,那么总共可以分为NS 个块。我们将所有区间左端点 L 按所属的块排序,L 相同时再按右端点 R 递增排序。在这样的顺序下,再用上文说过的双指针的暴力做法。此时我们再分析复杂度可以发现:

  1. 对于左端点指针,在每个块内移动的次数为 S ,移动 M 次;块间移动的次数为 S ,一共移动 NS 次。复杂度为 O(SM+N).
  2. 对于右端点指针,当左端点在同一块内时,移动 N 次,一共有 NS 个块。复杂度为 O(N2S).
  3. 如此一来,整个算法的时间复杂度为 O(SM+N+N2S) 。其中 N 移动小于 N2S ,予以忽略。又由基本不等式 a+b2ab 可知,SM+N2S2MN2=2NM ,当且仅当 SM=N2S 时,即 S=NMM 时,有最小复杂度 O(NM)

这就是莫队的实现,没错,就像之前说的,莫队是基于暴力的离线算法。

Code

点击查看代码
#include<algorithm> #include<cstdio> #include<cmath> using namespace std; const int N = 50000 + 10; const int M = 200000 + 10; const int S = 1000000 + 10; struct Query { int id, l, r; }; Query q[M]; int w[N], ans[M], cnt[S]; int n, m, len; inline int read() { int s = 0, f = 1; char c = getchar(); while (c < '0' || c > '9') { if (c == '-') f = -1; c = getchar(); } while (c >= '0' && c <= '9') { s = (s << 3) + (s << 1) + (c ^ 48); c = getchar(); } return s * f; } inline int Get(int x) { return x / len; } inline bool cmp(Query a, Query b) { int i, j; i = Get(a.l), j= Get(b.l); //取左端点所在区间 return i != j ? i < j : a.r < b.r; //先按块排,再右端点升序 } inline void Add(int x, int &res) { if (!cnt[x]) ++res; ++cnt[x]; } inline void Del(int x, int &res) { --cnt[x]; if (!cnt[x]) --res; } int main() { n = read(); for (int i = 1; i <= n; ++i) w[i] = read(); m = read(); len = max(1, (int) sqrt((double) n * n / m)); for (int i = 1; i <= m; ++i) { int l, r; l = read(), r = read(); q[i] = (Query) {i, l, r}; } sort(q + 1, q + m + 1, cmp); //排序 for (int k = 1, i = 0, j = 1, res = 0; k <= m; ++k) { while (i < q[k].r) Add(w[++i], res); while (i > q[k].r) Del(w[i--], res); while (j < q[k].l) Del(w[j++], res); while (j > q[k].l) Add(w[--j], res); //这四句及其精华,同时也比较容易写错,建议在纸上推一边理解 ans[q[k].id] = res; } for (int i = 1; i <= m; ++i) printf ("%d\n", ans[i]); return 0; }

__EOF__

本文作者Nebula
本文链接https://www.cnblogs.com/Nebula-Astraea/p/16215554.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   Nebula  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示