「学习笔记」莫队算法
0x10 莫队算法的概念
莫队算法是由莫涛提出的算法。莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。 ——摘自 \(Oi-wiki\)。
0x20 普通莫队算法
-
0x21 形式
给定一个序列 \(Q\) ,我们设 \(n\) 和 \(m\) 同阶。对于序列上的查询问题,给定查询区间 \([L_i,R_i]\),我们可以以 \(O(1)\) 的速度扩展答案到与查询区间相邻的四个区间,即:\([L_{i-1}, R]\), \([L_{i+1}, R]\) ,\([L_i, R_{i+1}]\) 和 \([L_i,R_{i-1}]\)。而我们通常设的块长为 \(\sqrt{n}\) ,那么我们的时间复杂度就是 \(O(n\sqrt{n})\),这对于我们 \(n\) 达到 \(10^5\) 时,是一个非常快速的做法,且代码实现较易。
代码模板如下:
for (int i = 1; i <= m; i ++) { while (r < mt[i].r) add (++ r); while (l > mt[i].l) add (-- l); while (l < mt[i].l) del (l ++); while (r > mt[i].r) del (r --); ans[mt[i].id] = res; }
-
0x22 实现
对于 \(m\) 个询问,我们将其先存储起来,然后再排序,最后离线处理这些数据。
-
0x23 排序
正常来说,我们的排序方法都是以 \([L_i, R_i]\) 所在的块的编号为第一关键字,以右端点为第二关键字从小到大排序。
代码实现如下:
inline bool cmp (MOTEAM x, MOTEAM y) { if (block[x.l] == block[y.l]) { //x和y的左端点都在同一个块中时,按右端点进行排序。 return x.r < y.r; } if (block[x.l] != block[y.l]) { //x和y的左端点不在同一个块中时,按左端点进行排序。 return x.l < y.l; } }
-
0x24 优化
在很多时候,我们会盲目的将块长设置为 \(\sqrt{n}\),所以如果遇到 \(m\) 和 \(\sqrt{n}\) 同阶时,而恰好我们将块长设置为 \(\sqrt{n}\) 时,这样我们就有可能被构造出的数据卡掉。
那我们应该将块长设为什么呢?在随机数据下,设为 \(\frac{n}{\sqrt{\frac{2}{3} m}}\) 会快一些,大概能优化 \(\frac{1}{10}\) 左右。证明留给读者自行探索。
我们发现,莫队算法实际上看上去仅仅是暴力的优化。但是为什么使用这种"看似暴力"的做法却可以通过高难度数据结构题目呢?很显然,我们的算法复杂度在离线进行排序的时候被优化了。所以,这一排序节省了我们很多的时间。
于是又有神仙创造出了一种新的排序方法:奇偶性排序。
首先,我们原来的排序方法,虽然让右端点的大跳动减少了很多。但是,右端点的跳动依旧存在,使我们的时间复杂度大幅度升高。我们能否创造一种方法排序使其更优化呢?
我们在选择奇数块时,按照右端点从小到大排序,在偶数块时从大到小排序,这样可以减少右端点进行大跳动的次数。
而为什么这样排序会快呢?是因为右指针移动到右边后就不需要跳回左边了,而跳回左边后处理下一个块是要跳动到右边的。很明显,这样就会优化近一半的时间复杂度。
代码实现如下:
inline bool cmp (MOTEAM x, MOTEAM y) { return block[x.l] == block[y.l] ? (block[y.l] & 1) ? x.r < y.r : x.r > y.r : x.l < y.l }
0x30 普通莫队算法例题
-
0x31 例题 \(1\)
很经典的一道题。这道题求 \([L_i,R_i]\) 区间只出现过一次的数字个数。这样我们的 \(add\) 和 \(del\) 函数就很好设计。
inline void add (int x) { cnt[a[x]] ++; if (cnt[a[x]] == 1) { res ++; } } inline void del (int x) { cnt[a[x]] --; if (cnt[a[x]] == 0) { res --; } }
但是这道题的出题人也许特意卡莫队做法,正常写法应该是 \(65pts\) ,会 \(TLE\) \(5\)个点。
所以我们需要大力卡常!我们运用上面的优化技巧,修改块长,使用奇偶性排序方法,将 \(add\) 和 \(del\) 改成位运算,提前预处理左端点所在块,吸口氧就可以通过该题了。
-
0x32 例题 \(2\)
这道题和上道题几乎相同,我们只需要用正常的莫队做法就可以通过。
核心代码如下:
inline void add(int x, int &res) { if (num[a[x]] == 0) { //次数未增加前该数次数出现次数为0,就没有出现过,即不重复。 res ++; } num[a[x]] ++; } inline void del(int x, int &res) { num[a[x]] --; if (num[a[x]] == 0) { //次数减少后该数次数出现次数为0,就出现过,个数减少。 res --; } }
-
0x33 例题 \(3\)
我们看到题目,每个数字贡献为出现次数的平方,这样我们的 \(add\) 和 \(del\) 函数就不太好编写。
这样我们就需要用到完全平方的小知识解决此题。
即:
\((a+1)^2 = a^2+1+2a\)
于是我们的 \(add\) 函数和 \(del\) 函数有这种写法:
inline void add (int x) { ans += cnt[a[x]] * 2 + 1; cnt[a[x]] ++; } inline void del (int x) { ans -= cnt[a[x]] * 2 - 1; cnt[a[x]] --; }
但是不要忘记,莫队是暴力美学,当然我们的 \(add\) 函数和 \(del\) 函数也可以暴力模拟。
核心代码如下:
inline void add (int x) { memo -= cnt[a[x]] * cnt[a[x]]; cnt[a[x]] ++; memo += cnt[a[x]] * cnt[a[x]]; } inline void del (int x) { memo -= cnt[a[x]] * cnt[a[x]]; cnt[a[x]] --; memo += cnt[a[x]] * cnt[a[x]]; }
-
0x34 例题 \(4\)
\(0x33\) 的双倍经验,直接计算答案贡献即可。
-
0x35 例题 \(5\)
模板题要做吐了qwq。
同样是统计区间内出现次数为 \(1\) 的数字个数,最后判断出现次数为 \(1\) 的数字个数是否为该区间所有数字个数就可以了。
核心代码如下:for (int i = 1; i <= q; i ++) { while (l < mt[i].l) del (l ++); while (r < mt[i].r) add (++ r); while (l > mt[i].l) add (-- l); while (r > mt[i].r) del (r --); if (res == mt[i].r - mt[i].l + 1) { ans[mt[i].id] = 1; } }
-
0x36 例题 \(6\)
CF617E XOR and Favorite Number
需要稍微了解异或的性质,其实还是一道普通莫队模板题。
设区间 \([l,r]\) 的异或和为 \(sun_{l,r}\),那么 \(sum_{l,r}=sum_{1,l}⊕sum_{1,r}\),也就是 \(sum_{l,r}=sum_{1,r}-sum_{1,l-1}\)。那么这样就可以 \(O(1)\) 求出区间异或和了。
根据异或的交换律和移项可以简单证明。
设 \(num\) 数组表示前缀异或值,那么根据 \(x⊕y=z\),有 \(x⊕z=y\),那么每次出现一个数 \(x\) 时,可以将 \(x⊕k\) 的出现次数加入答案。核心代码如下:
inline void add (int x) { res += num[(a[x] ^ k)]; num[a[x]] ++; } inline void del (int x) { num[a[x]] --; res -= num[(a[x] ^ k)]; }
-
0x37 例题 \(7\)
这是上道题的双倍经验,依旧是存储答案异或 \(k\) 后的数字,查询次数也需要查询异或 \(k\) 的次数,就可以了。
-
0x38 例题 \(8\)
CF220B Little Elephant and Array
普通莫队操作。\(add\) 和 \(del\) 时判断次数是否与元素相同即可。
核心代码如下:
inline void add (int x) { if (num[a[x]] == a[x]) { res --; } num[a[x]] ++; if (num[a[x]] == a[x]) { res ++; } } inline void del (int x) { if (num[a[x]] == a[x]) { res --; } num[a[x]] --; if (num[a[x]] == a[x]) { res ++; } }
0x40 带修莫队
-
0x41 带修莫队思路
普通的莫队算法是不支持修改操作的操作,但是我们可以强行让它修改,类似 \(dp\) 一样加上一维时间维,表示这次操作的时间。、
时间维表示经历的修改次数,询问就变成了 \([l,r,time]\)。
那坐标也可以在时间维上移动,那么 \([l,r,time]\) 可以变成:- \([l-1,r,time]\)
- \([l+1,r,time]\)
- \([l,r-1,time]\)
- \([l,r+1,time]\)
- \([l,r,time-1]\)
- \([l,r,time+1]\)
这样的转移也是 \(O(1)\) 的。
带修莫队时,将块长设为 \(n^{\frac{2}{3}}\),一共分成 \(n^{\frac{1}{3}}\) 块。第一关键字为左端点所在的块,第二关键字为右端点所在的块,第三关键字为时间。
时间复杂度为 \(O(n^{\frac{5}{3}})\) ,读者可以自证。 -
0x42 带修莫队例题
带修莫队的模板题。
上文我们提到,将查询操作按照 \([l,r,time]\) 的形式存储下来,分别为左端点、右端点、经历的修改次数。
然后还需要一个变量记录当前已经修改的次数。如果当前查询操作需要改的次数比当前多,那么改回去;否则改回来。
例如:已经进行了 \(3\) 次修改,本次查询是在第 \(5\) 次修改之后,那就执行第 \(4,5\) 次修改。这样就能避免修改对于答案的影响了。
完整代码如下:#include <bits/stdc++.h> using namespace std; const int N = 2e6 + 7; inline int read() { int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') { f = -1; } ch = getchar(); } while (ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } return x * f; } typedef long long ll; struct Query { int l; int r; int time; int id; }q[N]; struct Change { int pos; int v; }c[N]; int n, m, siz, res = 0, a[N], col[N], totq = 0, totc = 0, ans[N], block[N]; inline bool cmp (Query a, Query b) { if (block[a.l] == block[b.l]) { if (block[a.r] == block[b.r]) { return a.time < b.time; } else { return a.r < b.r; } } else { return a.l < b.l; } //第一关键字为左端点,第二关键字为右端点,第三关键字为时间 } inline void add (int i) { if (++ col[i] == 1) { res ++; } } inline void del (int i) { if (-- col[i] == 0) { res --; } } //常规add和del操作 inline void work (int now, int i) { if (c[now].pos >= q[i].l && c[now].pos <= q[i].r) { //只有修改在询问的区间内时,才会对答案产生影响 add (c[now].v); del (a[c[now].pos]); } swap (c[now].v, a[c[now].pos]); //修改是可逆的,下次再操作就恢复了,直接交换颜色即可 } int main () { scanf ("%d%d", &n, &m); siz = pow (n, 0.666); for (int i = 1; i <= n; i ++) { scanf ("%d", &a[i]); block[i] = (i - 1) / siz + 1; } while (m --) { char op[8]; scanf ("%s", op); if (op[0] == 'Q') { q[++ totq].l = read (); q[totq].r = read (); q[totq].time = totc; q[totq].id = totq; } else if (op[0] == 'R') { c[++ totc].pos = read (); c[totc].v = read (); } } sort (q + 1, q + 1 + totq, cmp); int l = 1, r = 0, now = 0; for (int i = 1; i <= totq; i ++) { while (l > q[i].l) { add (a[-- l]); } while (r < q[i].r) { add (a[++ r]); } while (l < q[i].l) { del (a[l ++]); } while (r > q[i].r) { del (a[r --]); } while (now < q[i].time) { work (++ now, i);//改少了,继续改 } while (now > q[i].time) { work (now --, i);//改多了,改回去 } ans[q[i].id] = res; } for (int i = 1; i <= totq; i ++) { printf ("%d\n", ans[i]); } return 0; }