最后更新于:2022年6月25日 下午
首先莫队算法是一个离散算法,复杂度 \(O(n \sqrt{m})\) 一般用于 \(m\) 次长为 \(n\) 的区间问题
首先按照左端点所在的块为第一关键字,再按照右端点升序为第二关键字排序。优化:如果都是从小到大,那么由于这样两个指标都是上升的(就会来来回回好几次),因此我们可以奇偶交替排序,即奇数块从小到大排序,偶数快从大到小排序
但是我们可以心中有分块,但是代码中不表现出来!(例如 OI-wiki 中示例代码),但是示例代码的做法不可避免的牵扯到除法,那还不如按照原始做法来,但是注意依然心中有块即可。
注意四个循环的位置,遵寻先放大区间再缩小区间的策略即可,先动做还是先动右无区别。
注意到 stl 排序中必须要保证 \(a < b\) 和 \(b < a\) 最多一个成立
模板例题:LOJ 1494 的优雅做法(这里 sn 可取 \(\frac{n}{\sqrt{m}}\):
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
| #include <bits/stdc++.h> #define watch(x) std::cout << (#x) << " is " << (x) << std::endl using LL = long long;
struct Node { int l, r, id, b; bool operator<(const Node & A) const { if (b != A.b) return l < A.l; if (b & 1) return r < A.r; return r > A.r; } };
int main() { std::cin.tie(nullptr)->sync_with_stdio(false); int n, m; std::cin >> n >> m; std::vector<int> a(n); for (auto &x : a) std::cin >> x; int sn = n / std::sqrt(m); std::vector<Node> b(m); for (int i = 0; i < m; ++i) { std::cin >> b[i].l >> b[i].r; --b[i].l; --b[i].r; b[i].b = b[i].l / sn; b[i].id = i; } std::sort(b.begin(), b.end()); LL cur = 0; std::vector<int> cnt(n + 1); auto add = [&](int x) { cur += cnt[x]; ++cnt[x]; }; auto del = [&](int x) { --cnt[x]; cur -= cnt[x]; }; std::vector<LL> f(n), g(n); int l = 0, r = -1; for (int i = 0; i < m; ++i) { if (b[i].l == b[i].r) { f[b[i].id] = 0, g[b[i].id] = 1; } else { while (l > b[i].l) add(a[--l]); while (r < b[i].r) add(a[++r]); while (l < b[i].l) del(a[l++]); while (r > b[i].r) del(a[r--]); f[b[i].id] = cur; g[b[i].id] = LL(r - l + 1) * (r - l) / 2; } } for (int i = 0; i < m; ++i) { if (f[i]) { LL d = std::__gcd(f[i], g[i]); f[i] /= d; g[i] /= d; } else g[i] = 1; std::cout << f[i] << '/' << g[i] << '\n'; } return 0; }
|
带修改的莫队能做到 \(O(n^{\frac{5}{3}})\),没啥学的兴趣了。树上莫队就是把树结构转化成线性结构
例题:LibreOJ-2874 的提交,如果用普通莫队的话,利用优先队列和 map 来加减操作会多加一个 log。因此我们这里使用回滚莫队。
回滚莫队的策略就是:如果需要考虑的左右端点在同一个块中,那么我们就直接暴力求解,这一类区间总复杂度不会超过 \(O(n \sqrt{n}\),这样分情况是为了能够做回滚,即让 l 在块的右端点,r 初始在块的右端点, l 在 r 右边挨着,然后始终保持 r 右移,l 左移动.
这里不用上面的奇偶优化,是因为这里真的要分块解决。不得不说我的代码写的真的优雅
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
| #include <bits/stdc++.h> #define watch(x) std::cout << (#x) << " is " << (x) << std::endl using LL = long long;
template<typename T> std::vector<T> discrete(std::vector<T>& a) { auto b = a; std::sort(b.begin(), b.end()); b.erase(std::unique(b.begin(), b.end()), b.end()); std::vector<T> r(b.size()); for (auto & x : a) { int id = std::lower_bound(b.begin(), b.end(), x) - b.begin(); r[id] = x; x = id; } return r; }
struct Node { int l, r, id, b; bool operator<(const Node & A) const { if (b != A.b) return l < A.l; return r < A.r; } };
int main() { std::cin.tie(nullptr)->sync_with_stdio(false); int n, m; std::cin >> n >> m; std::vector<int> a(n); for (auto &x : a) std::cin >> x; auto c = discrete(a); int sn = n / std::sqrt(m) + 1; std::vector<Node> b(m); for (int i = 0; i < m; ++i) { std::cin >> b[i].l >> b[i].r; --b[i].l; --b[i].r; b[i].b = b[i].l / sn; b[i].id = i; } std::sort(b.begin(), b.end()); std::vector<int> cnt(c.size()), tmpcnt(c.size()); auto add = [&](int x, LL &now) { ++cnt[x]; now = std::max(now, LL(c[x]) * cnt[x]); }; auto del = [&](int x) { --cnt[x]; }; std::vector<LL> ans(m); LL cur = 0, tmp = 0; for (int i = 0, l = 0, r = -1, lastB = -1; i < m; ++i) { if (b[i].b != lastB) { int BL = std::min((b[i].b + 1) * sn, n) - 1; while (r < BL) add(a[++r], cur); while (r > BL) del(a[r--]); while (l <= BL) del(a[l++]); cur = 0; lastB = b[i].b; } if (l > b[i].r) { for (int j = b[i].l; j <= b[i].r; ++j) ++tmpcnt[a[j]]; for (int j = b[i].l; j <= b[i].r; ++j) { ans[b[i].id] = std::max(ans[b[i].id], LL(c[a[j]]) * tmpcnt[a[j]]); } for (int j = b[i].l; j <= b[i].r; ++j) --tmpcnt[a[j]]; } else { while (r < b[i].r) add(a[++r], cur); tmp = cur; int nl = l; while (nl > b[i].l) add(a[--nl], tmp); ans[b[i].id] = tmp; while (nl < l) del(a[nl++]); } } for (auto x : ans) std::cout << x << '\n'; 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| #include <bits/stdc++.h> #define watch(x) std::cout << (#x) << " is " << (x) << std::endl using LL = long long;
struct Node { int l, r, id, b; bool operator<(const Node & A) const { if (b != A.b) return l < A.l; return r < A.r; } };
int main() { std::cin.tie(nullptr)->sync_with_stdio(false); int n, m; std::cin >> n >> m; std::vector<int> a(n); for (auto &x : a) std::cin >> x; int sn = n / std::sqrt(m) + 1; std::vector<Node> b(m); for (int i = 0; i < m; ++i) { std::cin >> b[i].l >> b[i].r; --b[i].l; --b[i].r; b[i].b = b[i].l / sn; b[i].id = i; } std::sort(b.begin(), b.end()); std::vector<int> cnt(n + 1), tmpcnt(n + 1); auto add = [&](int x, int &now) { now = std::max(now, ++cnt[x]); }; auto del = [&](int x) { --cnt[x]; }; std::vector<int> ans(m); int cur = 0; for (int i = 0, l = 0, r = -1, lastB = -1; i < m; ++i) { if (b[i].b != lastB) { int BL = std::min((b[i].b + 1) * sn, n) - 1; while (r < BL) add(a[++r], cur); while (r > BL) del(a[r--]); while (l <= BL) del(a[l++]); cur = 0; lastB = b[i].b; } if (l > b[i].r) { for (int j = b[i].l; j <= b[i].r; ++j) { ans[b[i].id] = std::max(ans[b[i].id], ++tmpcnt[a[j]]); } for (int j = b[i].l; j <= b[i].r; ++j) --tmpcnt[a[j]]; } else { while (r < b[i].r) add(a[++r], cur); int tmp = cur, nl = l; while (nl > b[i].l) add(a[--nl], tmp); ans[b[i].id] = tmp; while (nl < l) del(a[nl++]); } ans[b[i].id] = std::max(1, 2 * ans[b[i].id] - (b[i].r - b[i].l + 1)); } for (auto x : ans) std::cout << x << '\n'; return 0; }
|
后来遇到的莫队问题
- P4137:普通莫队会被特殊数据 gank,带 log 的普通莫队也会被随机数据卡掉。因此只能用回滚莫队啦,并且是删除版本的回滚莫队!这里删除版本不用分情况讨论。提交记录