莫队算法简介 莫队算法是由莫涛提出的算法。在莫涛提出莫队算法之前,莫队算法已经在 Codeforces 的高手圈里小范围流传,但是莫涛是第一个对莫队算法进行详细归纳总结的人。莫涛提出莫队算法时,只分析了普通莫队算法,但是经过 OIer 和 ACMer 的集体智慧改造,莫队有了多种扩展版本。
莫队算法可以解决一类离线区间询问问题,基于双指针,分块,排序等思想,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。
普通莫队 莫队是处理离线区间询问的算法$O(n \sqrt n)$
假设$n = m$ ($m$为询问数,$n$为区间长度),对于任意一个区间$[l,r]$ 我们假设已经知道这个区间的$val$,我们可以通过拓展到$[l-1,r],[l+1,r],[l,r-1],[l,r+1]$这四个区间,并且算出对应的$val$,那么我们的效率将会变得很高。
为了让我们的拓展次数变少,我们可以对每个询问进行排序,尽可能的让相近的区间同时查询。这就是莫队的一个很神奇的地方,采用了分块的思想,把区间分成一块一块的,这样我们的排序规则就是,先按照所属块的先后排序,然后同一块的再按照右端点排序,这样就大大降低了我们的时间复杂度。
实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void move (int pos, int sign) { }void solve () { BLOCK_SIZE = int (ceil (pow (n, 0.5 ))); sort (querys, querys + m); for (int i = 0 ; i < m; ++i) { const query &q = querys[i]; while (l > q.l) move (--l, 1 ); while (r < q.r) move (++r, 1 ); while (l < q.l) move (l++, -1 ); while (r > q.r) move (r--, -1 ); ans[q.id] = nowAns; } }
复杂度分析 莫队的复杂度证明比较复杂,这里贴上OI Wiki 莫队算法还有一个特点:当 n 不变时,m 越大,处理每次询问的平均转移代价就越小。一些其他的离线算法也具有同样的特点(如求 LCA 的 Tarjan 算法),但是莫队算法的平均转移代价随 m 的变化最明显。
排序优化 1 2 3 4 5 // 假设块的大小为2 1 1 2 100 3 1 4 100
我们通过上面的数据可以发现,r指针的移动次数大概为300次,并且每一个区间的r,导致整体看起来像一个波浪,这里我们就要用到奇偶化排序。
什么是奇偶化排序?奇偶化排序即对于属于奇数块的询问,r 按从小到大排序,对于属于偶数块的排序,r 从大到小排序,这样我们的 r 指针在处理完这个奇数块的问题后,将在返回的途中处理偶数块的问题,再向 n 移动处理下一个奇数块的问题,优化了 r 指针的移动次数,一般情况下,这种优化能让程序快 30% 左右。
1 2 3 4 5 sort (query.begin () + 1 , query.end (), [&](auto x, auto y) { if (K[x[0 ]] != K[y[0 ]]) return x[0 ] < y[0 ]; if (K[x[0 ]] & 1 ) return x[1 ] < y[1 ]; return x[1 ] > y[1 ]; });
例题 我们先拿最简单的应用,前缀和,众所周知,有很多方法都可以求前缀和,当然莫队也行。
前缀和 输入一个长度为 $n$ 的整数序列。
接下来再输入 $m$ 个询问,每个询问输入一对 $l, r$。
对于每个询问,输出原序列中从第 $l$ 个数到第 $r$ 个数的和。
输入格式 第一行包含两个整数 $n$ 和 $m$。
第二行包含 $n$ 个整数,表示整数数列。
接下来 $m$ 行,每行包含两个整数 $l$ 和 $r$,表示一个询问的区间范围。
输出格式 共 $m$ 行,每行输出一个询问的结果。
数据范围 $1 \le l \le r \le n$, $1 \le n,m \le 100000$, $-1000 \le 数列中元素的值 \le 1000$
输入样例: 5 3 2 1 3 6 4 1 2 1 3 2 4
输出样例: 3 6 10 对于每一个区间,我们考虑那四种情况:
当左边需要增加一个位置,我们直接加上那个位置的值
当左边需要减少一个位置,我们直接减少那个位置的值
当右边需要增加一个位置,我们直接加上那个位置的值
当右边需要减少一个位置,我们直接减少那个位置的值
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> #include <queue> #include <deque> #include <stack> #include <unordered_map> #include <unordered_set> #include <numeric> #include <iomanip> #define _fio ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); #define pb(e) push_back(e) #define all(x) (x).begin(), (x).end() #define allr(x) (x).rbegin(), (x).rend() #define endl '\n' using namespace std;using i64 = long long ;using PII = pair<int ,int >;const int INF = 0x3f3f3f3f ;int main () { int n,m; cin >> n >> m; vector<int > v (n + 1 ) ; vector<array<int ,3> > query (m+1 ); for (int i=1 ;i<=n;++i) cin >> v[i]; for (int i=1 ;i<=m;++i) { int l,r;cin >> l >> r; query[i] = {l,r,i}; } int kn = n / min <int >(n,sqrt (m)); vector<int > K (n + 1 ) ; for (int i=1 ;i<=n;++i) K[i] = (i - 1 ) / kn + 1 ; sort (query.begin () + 1 ,query.end (),[&](auto x,auto y){ if (K[x[0 ]] != K[y[0 ]]) return x[0 ] < y[0 ]; if (K[x[0 ]] & 1 ) return x[1 ] < y[1 ]; return x[1 ] > y[1 ]; }); int l = 1 ,r = 0 ,val = 0 ; vector<int > ans (m + 1 ) ; for (int i=1 ;i<=m;++i) { auto &e = query[i]; int ql = e[0 ],qr = e[1 ],id = e[2 ]; auto add = [&](int x) -> void { val += x; }; auto sub = [&](int x) -> void { val -= x; }; while (l > ql) add (v[-- l]); while (r < qr) add (v[++ r]); while (l < ql) sub (v[l ++ ]); while (r > qr) sub (v[r -- ]); ans[id] = val; } for (int i=1 ;i<=m;++i) cout << ans[i] << endl; return 0 ; }
小 Z 的袜子 然后是P1494 [国家集训队] 小 Z 的袜子 - 洛谷
其实莫队的写法基本上不同就只是在$add,sub$这两个函数之中。 这道题需要额外维护一个$cnt$数组,因为我们需要知道的是这个区间,相同颜色的袜子的数量,之后我们如何去算概率,首先我们考虑分母,一个完整的区间$[l,r]$,假如区间长度为$n$所有的组合次数为$C_n^2$,对应每一个分子,我们先考虑$add$加上颜色为$k$的袜子的情况,我们增加了一个$k$假如我们原来一共有$f$只,就是$C_f^2$现在我们需要更新了,因为多了一只,所以我的现在的情况即为$C_{f+1}^2$ ,然后我们再减去原来的。即为 $$ C_{f+1}^2 - C_f^2 = (f+1)*f / 2 - f * (f - 1) / 2 $$ $sub$同理,其实这个公式还可以简化,简化为$f$,但是为了清晰,下方代码并没有简化。
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> #include <queue> #include <deque> #include <stack> #include <unordered_map> #include <unordered_set> #include <numeric> #include <iomanip> #define _fio ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); #define pb(e) push_back(e) #define all(x) (x).begin(), (x).end() #define allr(x) (x).rbegin(), (x).rend() #define endl '\n' using namespace std;using i64 = long long ;using PII = pair<int ,int >;const int INF = 0x3f3f3f3f ;i64 gcd (i64 a,i64 b) { return b ? gcd (b,a % b) : a; }int main () { _fio int n,m; cin >> n >> m; vector<int > v (n + 1 ) ,cnt (n + 1 ) ; vector<array<int ,3> > query (m+1 ); for (int i=1 ;i<=n;++i) cin >> v[i]; for (int i=1 ;i<=m;++i) { int l,r;cin >> l >> r; query[i] = {l,r,i}; } int kn = n / min <int >(n,sqrt (m)); vector<int > K (n + 1 ) ; for (int i=1 ;i<=n;++i) K[i] = (i - 1 ) / kn + 1 ; sort (query.begin () + 1 ,query.end (),[&](auto x,auto y){ if (K[x[0 ]] != K[y[0 ]]) return x[0 ] < y[0 ]; if (K[x[0 ]] & 1 ) return x[1 ] < y[1 ]; return x[1 ] > y[1 ]; }); int l = 1 ,r = 0 ; i64 val = 0 ; vector<i64> ans1 (m + 1 ) ,ans2 (m + 1 ) ; for (int i=1 ;i<=m;++i) { auto &e = query[i]; int ql = e[0 ],qr = e[1 ],id = e[2 ]; if (ql == qr) { ans1[id] = 0 ,ans2[id] = 1 ; continue ; } auto add = [&](int x) -> void { val += (cnt[x] + 1 )*cnt[x]/2 - cnt[x]*(cnt[x] - 1 )/2 ; cnt[x] ++ ; }; auto sub = [&](int x) -> void { cnt[x] -- ; val -= (cnt[x] + 1 )*cnt[x]/2 - cnt[x]*(cnt[x] - 1 )/2 ; }; while (l > ql) add (v[-- l]); while (r < qr) add (v[++ r]); while (l < ql) sub (v[l ++ ]); while (r > qr) sub (v[r -- ]); ans1[id] = val; ans2[id] = (i64)(r - l + 1 ) * (r - l) / 2 ; } for (int i=1 ;i<=m;++i) { if (ans1[i] != 0 ) { i64 g = gcd (ans1[i],ans2[i]); ans1[i]/=g,ans2[i]/=g; } else { ans2[i] = 1 ; } cout << ans1[i] << "/" << ans2[i] << endl; } return 0 ; }
XOR and Favorite Number Problem - 617E - Codeforces
题面翻译
给定一个长度为 $n$ 的序列 $a$,然后再给一个数字 $k$,再给出 $m$ 组询问,每组询问给出一个区间,求这个区间里面有多少个子区间的异或值为 $k$。 $1 \le n,m \le 10 ^ 5$,$0 \le k,a_i \le 10^6$,$1 \le l_i \le r_i \le n$。 我们要求$a_i \bigoplus a_{i+1} \bigoplus a_{i+2} \bigoplus … \bigoplus a_j$,可以通过前缀和,因为异或也满足类似前缀和的性质。 所以这个式子就变成了$(a_1 \bigoplus a_2 \bigoplus … \bigoplus a_{i-1}) \bigoplus (a_1 \bigoplus a_2 \bigoplus … \bigoplus a_j)$ ,如果用前缀和异或数组的话,这道题就变成了,有多少个数对$(i,j)$满足$a_i \bigoplus a_j = k$。之后直接莫队,因为$x \bigoplus y = z -> x \bigoplus z = y$ 所以就有我们每加入一个$x$,每一个对应的y都满足,所以把y的数量给加上,用$cnt$数组当做桶就行。
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> #include <queue> #include <deque> #include <stack> #include <unordered_map> #include <unordered_set> #include <numeric> #include <iomanip> #define _fio \ ios_base::sync_with_stdio(0); \ cin.tie(0); \ cout.tie(0); #define pb(e) push_back(e) #define all(x) (x).begin(), (x).end() #define allr(x) (x).rbegin(), (x).rend() #define endl '\n' using namespace std;using i64 = long long ;using PII = pair<int , int >;const int INF = 0x3f3f3f3f ;int main () { _fio int n, k, m; cin >> n >> m >> k; i64 kn = n / min <int >(n, sqrt (m)); vector<int > v (n + 1 ) , K (n + 1 ) ,cnt (2e6 +10 ) ; vector<i64> ans (m + 1 ) ; vector<array<int , 3>> query (m + 1 ); for (int i = 1 ; i <= n; ++i) cin >> v[i], v[i] ^= v[i - 1 ], K[i] = (i - 1 ) / kn + 1 ; for (int i = 1 ; i <= m; ++i) { i64 l, r; cin >> l >> r; query[i] = {l - 1 , r, i}; } sort (query.begin () + 1 , query.end (), [&](auto x, auto y) { if (K[x[0 ]] != K[y[0 ]]) return x[0 ] < y[0 ]; if (K[x[0 ]] & 1 ) return x[1 ] < y[1 ]; return x[1 ] > y[1 ]; }); int l = 1 , r = 0 ; i64 val = 0 ; for (int i = 1 ; i <= m; ++i) { auto &e = query[i]; int ql = e[0 ], qr = e[1 ], id = e[2 ]; auto add = [&](int x) -> void { val += (i64)cnt[x ^ k]; cnt[x]++; }; auto sub = [&](int x) -> void { cnt[x]--; val -= (i64)cnt[x ^ k]; }; while (l > ql) add (v[--l]); while (r < qr) add (v[++r]); while (l < ql) sub (v[l++]); while (r > qr) sub (v[r--]); ans[id] = val; } for (int i = 1 ; i <= m; ++i) cout << ans[i] << endl; return 0 ; }
算法模板
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 int main () { int n, m; cin >> n >> m; i64 kn = n / min <int >(n, sqrt (m)); vector<int > v (n + 1 ) , K (n + 1 ) ; vector<i64> ans (m + 1 ) ; vector<array<int , 3>> query (m + 1 ); for (int i = 1 ; i <= n; ++i) cin >> v[i], K[i] = (i - 1 ) / kn + 1 ; for (int i = 1 ; i <= m; ++i) { int l, r; cin >> l >> r; query[i] = {l, r, i}; } sort (query.begin () + 1 , query.end (), [&](auto x, auto y) { if (K[x[0 ]] != K[y[0 ]]) return x[0 ] < y[0 ]; if (K[x[0 ]] & 1 ) return x[1 ] < y[1 ]; return x[1 ] > y[1 ]; }); int l = 1 , r = 0 , val = 0 ; for (int i = 1 ; i <= m; ++i) { auto &e = query[i]; int ql = e[0 ], qr = e[1 ], id = e[2 ]; auto add = [&](int x) -> void { }; auto sub = [&](int x) -> void { }; while (l > ql) add (v[--l]); while (r < qr) add (v[++r]); while (l < ql) sub (v[l++]); while (r > qr) sub (v[r--]); ans[id] = val; } for (int i = 1 ; i <= m; ++i) cout << ans[i] << endl; return 0 ; }