莫队
莫队
假设 n,m 同阶,对于序列上的区间询问问题,如果得知 [l,r] 的答案,可以在 O(1) 的时间推算出 [l−1,r],[l+1,r],[l,r−1],[l,r+1] 的答案,那么我们就可以在 O(n√n) 的时间求出所有询问的答案。
普通莫队
将所有的询问离线后以左端点所在的块为第一关键字,右端点为第二关键字进行排序。按排序后的顺序处理每一个询问,暴力从上一个区间的答案推出当前区间的答案。
注意移动指针时应先扩大区间, 后缩小区间,时间复杂度 O(n√n) 。
for (int i = 1, l = 1, r = 0; i <= m; ++i) { while (l > qry[i].l) add(--l); while (r < qry[i].r) add(++r); while (l < qry[i].l) del(l++); while (r > qry[i].r) del(r--); ans[qry[i].id] = result; }
优化:
- 调整块长。
- 奇偶性排序:在选择奇数块时,按照右端点从小到大排序,在偶数块时从大到小排序,这样可以减少右端点进行大跳动的次数,常数优化为原来的一半。
- 原理:因为右指针移动到右边后就不需要跳回左边了,而跳回左边后处理下一个块是要跳动到右边的。
P1494 [国家集训队] 小 Z 的袜子
给定 a1∼n ,m 次询问区间内任选两个数相同的概率。
n,m≤5×104
将题意用式子可以表示为:
问题就转化为求区间数字出现次数平方和。
考虑答案的增量:
于是就可以很轻松地设计 add
函数和 del
函数了。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 5e4 + 7; struct Query { int l, r, bid, id; inline bool operator < (const Query &rhs) const { return bid == rhs.bid ? (bid & 1 ? r > rhs.r : r < rhs.r) : bid < rhs.bid; } } qry[N]; pair<ll, ll> ans[N]; int a[N], cnt[N]; int n, m; signed main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; ++i) scanf("%d", a + i); int block = sqrt(n); for (int i = 1; i <= m; ++i) scanf("%d%d", &qry[i].l, &qry[i].r), qry[i].bid = qry[i].l / block, qry[i].id = i; sort(qry + 1, qry + m + 1); ll result = 0; auto update = [&](int x, int k) { result -= 1ll * cnt[x] * cnt[x]; cnt[x] += k; result += 1ll * cnt[x] * cnt[x]; }; for (int i = 1, l = 1, r = 0; i <= m; ++i) { if (qry[i].l == qry[i].r) { ans[qry[i].id] = make_pair(0, 1); continue; } while (l > qry[i].l) update(a[--l], 1); while (r < qry[i].r) update(a[++r], 1); while (l < qry[i].l) update(a[l++], -1); while (r > qry[i].r) update(a[r--], -1); ans[qry[i].id] = make_pair(result - (r - l + 1), 1ll * (r - l + 1) * (r - l)); } for (int i = 1; i <= m; ++i) { ll g = __gcd(ans[i].first, ans[i].second); printf("%lld/%lld\n", ans[i].first / g, ans[i].second / g); } return 0; }
CF617E XOR and Favorite Number
给定 k ,m 次询问区间内有多少子段异或和为 k 。
n,m≤105 ,k,ai≤106
可以先预处理出前缀异或值,由前缀和性质可知,问题转化为有多少对 i,j 满足 si⊕sj=k 。注意到 x⊕y=k 等价于 x⊕k=y 。于是问题转化为区间数字出现次数。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 1e5 + 7, V = 2e6 + 7; struct Query { int l, r, id, bid; inline bool operator < (const Query &rhs) const { return bid == rhs.bid ? (bid & 1 ? r > rhs.r : r < rhs.r) : l < rhs.l; } } qry[N]; ll ans[N]; int a[N], cnt[V]; ll result; int n, m, k, block; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline void add(int x) { result += cnt[a[x] ^ k], ++cnt[a[x]]; } inline void del(int x) { --cnt[a[x]], result -= cnt[a[x] ^ k]; } signed main() { n = read(), m = read(), k = read(), block = n / sqrt(m) + 1; for (int i = 1; i <= n; ++i) a[i] = a[i - 1] ^ read(); for (int i = 1; i <= m; ++i) qry[i].l = read() - 1, qry[i].r = read(), qry[i].id = i, qry[i].bid = qry[i].l / block; sort(qry + 1, qry + 1 + m); for (int i = 1, l = 1, r = 0; i <= m; ++i) { while (l > qry[i].l) add(--l); while (r < qry[i].r) add(++r); while (l < qry[i].l) del(l++); while (r > qry[i].r) del(r--); ans[qry[i].id] = result; } for (int i = 1; i <= m; ++i) printf("%lld\n", ans[i]); return 0; }
带修莫队
强行加上一维时间维,表示该操作的时间。在查询时修改,改多了就还原回来,改少了就改过去。
待修莫队的排序通常是这么排:
struct Query { int l, r, t, id; inline bool operator < (const Query &rhs) const { return bel[l] == bel[rhs.l] ? (bel[r] == bel[rhs.r] ? t < rhs.t : r < rhs.r) : l < rhs.l; } } qry[N];
设 n 为序列长度,m 个询问, t 个修改,则最佳块长为 3√n2tm ,实际操作取块长为 n23 即可。当 n,m,t 同数量级时时间复杂度为 O(n53) 。
P1903 [国家集训队] 数颜色 / 维护队列
给出序列 a1∼n ,m 次操作,操作有:
Q l r
:求 al∼r 中颜色总数。R p c
:将 ap 改为颜色 c 。n,m≤133333
注意一下修改操作 (x,k) 的影响,当且仅当 x∈[l,r] 才要统计。
考虑如何撤销操作,每次直接交换 ax 和 k ,这样下次修改就相当于撤销了。
#include <bits/stdc++.h> using namespace std; const int N = 1.5e5 + 7, V = 1e6 + 7; int bel[N]; struct Query { int l, r, t, id; inline bool operator < (const Query &rhs) const { return bel[l] == bel[rhs.l] ? (bel[r] == bel[rhs.r] ? t < rhs.t : r < rhs.r) : l < rhs.l; } } qry[N]; pair<int, int> upd[N]; int a[N], cnt[V], ans[N]; int n, m, cntu, cntq; signed main() { ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); cin >> n >> m; int block = pow(n, 0.667); for (int i = 1; i <= n; ++i) cin >> a[i], bel[i] = i / block; for (int i = 1; i <= m; ++i) { char op; int x, y; cin >> op >> x >> y; if (op == 'R') upd[++cntu] = make_pair(x, y); else ++cntq, qry[cntq] = (Query) {x, y, cntu, cntq}; } sort(qry + 1, qry + cntq + 1); for (int i = 1, l = 1, r = 0, t = 0, result = 0; i <= cntq; ++i) { auto update = [&](int x, int k) { if (!cnt[x]) ++result; cnt[x] += k; if (!cnt[x]) --result; }; while (l > qry[i].l) update(a[--l], 1); while (r < qry[i].r) update(a[++r], 1); while (l < qry[i].l) update(a[l++], -1); while (r > qry[i].r) update(a[r--], -1); auto modify = [&](int x) { if (l <= upd[x].first && upd[x].first <= r) update(a[upd[x].first], -1), update(upd[x].second, 1); swap(a[upd[x].first], upd[x].second); }; while (t < qry[i].t) modify(++t); while (t > qry[i].t) modify(t--); ans[qry[i].id] = result; } for (int i = 1; i <= cntq; ++i) printf("%d\n", ans[i]); return 0; }
树上莫队
通常用于处理树上路径信息统计问题,考虑将树的括号序跑下来,在括号序上跑莫队。
设点 i 入栈时时间戳为 sti ,出栈时时间戳为 edi ,则对于查询 u→v 路径上的信息分类讨论(钦定 dfnu<dfnv ):
- u 是 v 的祖先:查询 [stu,stv] 即可。
- u 不是 v 的祖先:查询 [edu,stv] 和 LCA(u,v) 即可。
注意查询时应忽略出现两次的点,括号序要开两倍内存。
时间复杂度和普通莫队是一样的。
SP10707 COT2 - Count on a tree II
给定一棵 n 个点的树,m 次查询 u→v 路径上节点颜色种数。
n≤4×104,m≤105
模板,下给出参考代码。
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7; struct Graph { vector<int> e[N]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G; struct Query { int l, r, lca, id, bid; inline bool operator < (const Query &rhs) const { return bid == rhs.bid ? (bid & 1 ? r > rhs.r : r < rhs.r) : l < rhs.l; } } qry[N]; int a[N], fa[N], dep[N], siz[N], son[N], top[N], st[N], ed[N], seq[N], cnt[N], ans[N]; bool tag[N]; int n, m, block, dfstime, result; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } void dfs1(int u, int f) { fa[u] = f, dep[u] = dep[f] + 1, siz[u] = 1; int mxsiz = -1; for (int v : G.e[u]) { if (v == f) continue; dfs1(v, u), siz[u] += siz[v]; if (siz[v] > mxsiz) son[u] = v, mxsiz = siz[v]; } } void dfs2(int u, int topf) { top[u] = topf, seq[st[u] = ++dfstime] = u; if (son[u]) dfs2(son[u], topf); for (int v : G.e[u]) if (v != fa[u] && v != son[u]) dfs2(v, v); seq[ed[u] = ++dfstime] = u; } inline int LCA(int x, int y) { while (top[x] != top[y]) { if (dep[top[x]] < dep[top[y]]) swap(x, y); x = fa[top[x]]; } return dep[x] < dep[y] ? x : y; } inline void add(int x) { if (!cnt[a[x]]) ++result; ++cnt[a[x]]; } inline void del(int x) { --cnt[a[x]]; if (!cnt[a[x]]) --result; } inline void update(int x) { if (tag[x]) del(x), tag[x] = false; else add(x), tag[x] = true; } signed main() { n = read(), m = read(); vector<int> vec; for (int i = 1; i <= n; ++i) vec.emplace_back(a[i] = read()); sort(vec.begin(), vec.end()); vec.erase(unique(vec.begin(), vec.end()), vec.end()); for (int i = 1; i <= n; ++i) a[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin(); for (int i = 1; i < n; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } dfs1(1, 0), dfs2(1, 1); block = n * 2 / sqrt(m * 0.667); for (int i = 1; i <= m; ++i) { int x = read(), y = read(); if (st[x] > st[y]) swap(x, y); qry[i].id = i, qry[i].lca = LCA(x, y); if (qry[i].lca == x) qry[i].l = st[x], qry[i].r = st[y], qry[i].lca = 0; else qry[i].l = ed[x], qry[i].r = st[y]; qry[i].bid = qry[i].l / block; } sort(qry + 1, qry + 1 + m); for (int i = 1, l = 1, r = 0; i <= m; ++i) { while (l > qry[i].l) update(seq[--l]); while (r < qry[i].r) update(seq[++r]); while (l < qry[i].l) update(seq[l++]); while (r > qry[i].r) update(seq[r--]); if (qry[i].lca) update(qry[i].lca); ans[qry[i].id] = result; if (qry[i].lca) update(qry[i].lca); } for (int i = 1; i <= m; ++i) printf("%d\n", ans[i]); return 0; }
P4074 [WC2013] 糖果公园
给定一棵树,q 次询问 u→v 路径上 ∑aci∑cntcij=1wi 的值,其中 aci 表示颜色 ci 的价值,cntci 表示 ci 出现次数,wi 表示出现 i 次的价值。
n,m,q≤105
树上带修莫队模板,下给出参考代码。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 2e5 + 7, LOGN = 19; struct Graph { vector<int> e[N]; inline void insert(int u, int v) { e[u].emplace_back(v); } } G; int bel[N]; struct Query { int l, r, lca, t, id; inline bool operator < (const Query &rhs) const { return bel[l] == bel[rhs.l] ? (bel[r] == bel[rhs.r] ? t < rhs.t : r < rhs.r) : l < rhs.l; } } qry[N]; pair<int, int> upd[N]; ll ans[N]; int fa[N][LOGN], dep[N], st[N], ed[N], seq[N], cnt[N]; int v[N], w[N], c[N]; bool tag[N]; int n, m, q, dfstime, cntu, cntq; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } void dfs(int u, int f) { fa[u][0] = f, dep[u] = dep[f] + 1, seq[st[u] = ++dfstime] = u; for (int i = 1; i < LOGN; ++i) fa[u][i] = fa[fa[u][i - 1]][i - 1]; for (int v : G.e[u]) if (v != f) dfs(v, u); seq[ed[u] = ++dfstime] = u; } inline int LCA(int x, int y) { if (dep[x] < dep[y]) swap(x, y); for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1) if (h & 1) x = fa[x][i]; if (x == y) return x; for (int i = LOGN - 1; ~i; --i) if (fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i]; return fa[x][0]; } signed main() { n = read(), m = read(), q = read(); for (int i = 1; i <= m; ++i) v[i] = read(); for (int i = 1; i <= n; ++i) w[i] = read(); for (int i = 1; i < n; ++i) { int u = read(), v = read(); G.insert(u, v), G.insert(v, u); } for (int i = 1; i <= n; ++i) c[i] = read(); dfs(1, 0); int block = pow(dfstime, 0.667); for (int i = 1; i <= dfstime; ++i) bel[i] = i / block; for (int i = 1; i <= q; ++i) { int op = read(), x = read(), y = read(); if (op) { if (st[x] > st[y]) swap(x, y); qry[++cntq].t = cntu, qry[cntq].id = cntq, qry[cntq].lca = LCA(x, y); if (qry[cntq].lca == x) qry[cntq].l = st[x], qry[cntq].r = st[y], qry[cntq].lca = 0; else qry[cntq].l = ed[x], qry[cntq].r = st[y]; } else upd[++cntu] = make_pair(x, y); } sort(qry + 1, qry + 1 + cntq); ll result = 0; for (int i = 1, l = 1, r = 0, t = 0; i <= cntq; ++i) { auto update = [&](int x) { if (tag[x]) result -= 1ll * v[c[x]] * w[cnt[c[x]]--], tag[x] = false; else result += 1ll * v[c[x]] * w[++cnt[c[x]]], tag[x] = true; }; while (l > qry[i].l) update(seq[--l]); while (r < qry[i].r) update(seq[++r]); while (l < qry[i].l) update(seq[l++]); while (r > qry[i].r) update(seq[r--]); auto modify = [&](int t) { if (tag[upd[t].first]) update(upd[t].first), swap(c[upd[t].first], upd[t].second), update(upd[t].first); else swap(c[upd[t].first], upd[t].second); }; while (t < qry[i].t) modify(++t); while (t > qry[i].t) modify(t--); if (qry[i].lca) update(qry[i].lca); ans[qry[i].id] = result; if (qry[i].lca) update(qry[i].lca); } for (int i = 1; i <= cntq; ++i) printf("%lld\n", ans[i]); return 0; }
回滚莫队
大前提:一类可离线问题。
- 有些信息区间伸长时很好维护,区间缩短时却不好维护。
- 有些信息区间缩短时很好维护,区间伸长时却不好维护。
实现
只增不删回滚莫队
首先将询问排序,应保证左端点在同一块内的询问的右端点单调不降。
若区间端点在同一块内,直接暴力解决即可。
否则每次处理询问时,令 l 指针指向块尾,r 指针指向块尾。先移动 r 指针,由于右端点不降,于是只要插入即可。保留下来信息,后移动 l 指针求解,处理完后用前面保留下来的信息恢复即可。
只删不增回滚莫队
首先将询问排序,应保证左端点在同一块内的询问的右端点单调不升。
若区间端点在同一块内,直接暴力解决即可。
否则每次处理询问时,令 l 指针指向块头,r 指针指向 n 。先移动 r 指针,由于右端点不升,于是只要删除即可。保留下来信息,后移动 l 指针求解,处理完后用前面保留下来的信息恢复即可。
时间复杂度分析
设分块大小为 B ,则时间复杂度为 O(mB+n2B) ,当 B=n√m 时复杂度最优为 O(n√m) 。
应用
JOISC2014 歴史の研究
m 次询问区间内 maxai×cntai 。
n,m≤105
模板题,下给出参考代码。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 2e5 + 7; int bid[N]; struct Query { int l, r, id; inline bool operator < (const Query &b) const { return bid[l] == bid[b.l] ? r < b.r : l < b.l; } } qry[N]; vector<int> vec; ll ans[N]; int a[N], L[N], R[N], cnt[N], BFcnt[N]; ll result; int n, m, block, tot; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline void prework() { block = pow(n, 0.666) + 1; while (++tot) { L[tot] = R[tot - 1] + 1, R[tot] = min(block * tot, n); fill(bid + L[tot], bid + R[tot] + 1, tot); if (R[tot] == n) break; } } inline ll BruteForce(int l, int r) { ll BFres = 0; for (int i = l; i <= r; ++i) BFres = max(BFres, 1ll * (++BFcnt[a[i]]) * vec[a[i]]); for (int i = l; i <= r; ++i) --BFcnt[a[i]]; return BFres; } signed main() { n = read(), m = read(); prework(); for (int i = 1; i <= n; ++i) vec.emplace_back(a[i] = read()); sort(vec.begin(), vec.end()); vec.erase(unique(vec.begin(), vec.end()), vec.end()); for (int i = 1; i <= n; ++i) a[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin(); for (int i = 1; i <= m; ++i) qry[i].l = read(), qry[i].r = read(), qry[i].id = i; sort(qry + 1, qry + 1 + m); for (int i = 1, pos = 1; i <= tot; ++i) { memset(cnt, 0, sizeof(int) * vec.size()); result = 0; for (int r = R[i], l = R[i] + 1; pos <= m && bid[qry[pos].l] == i; ++pos) { if (bid[qry[pos].r] == i) { ans[qry[pos].id] = BruteForce(qry[pos].l, qry[pos].r); continue; } while (r < qry[pos].r) ++cnt[a[++r]], result = max(result, 1ll * cnt[a[r]] * vec[a[r]]); ll nowresult = result; while (l > qry[pos].l) ++cnt[a[--l]], nowresult = max(nowresult, 1ll * cnt[a[l]] * vec[a[l]]); ans[qry[pos].id] = nowresult; while (l <= R[i]) --cnt[a[l++]]; } } for (int i = 1; i <= m; ++i) printf("%lld\n", ans[i]); return 0; }
P5906 【模板】回滚莫队&不删除莫队
m 次询问区间中相同的数的最大出现下标差。
n,m≤2×105
模板题,下给出参考代码。
#include <bits/stdc++.h> using namespace std; const int N = 2e5 + 7; int bid[N]; struct Query { int l, r, id; inline bool operator < (const Query &b) const { return bid[l] == bid[b.l] ? r < b.r : l < b.l; } } qry[N]; vector<int> vec; int a[N], L[N], R[N], lpos[N], rpos[N], BFlpos[N], BFrpos[N], ans[N]; int n, m, block, tot; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline void prework() { block = pow(n, 0.666) + 1; while (++tot) { L[tot] = R[tot - 1] + 1, R[tot] = min(block * tot, n); fill(bid + L[tot], bid + R[tot] + 1, tot); if (R[tot] == n) break; } } inline int BruteForce(int l, int r) { int result = 0; for (int i = l; i <= r; ++i) { BFlpos[a[i]] = min(BFlpos[a[i]], i), BFrpos[a[i]] = max(BFrpos[a[i]], i); result = max(result, BFrpos[a[i]] - BFlpos[a[i]]); } for (int i = l; i <= r; ++i) BFlpos[a[i]] = n + 1, BFrpos[a[i]] = 0; return result; } signed main() { n = read(), prework(); for (int i = 1; i <= n; ++i) vec.emplace_back(a[i] = read()); sort(vec.begin(), vec.end()); vec.erase(unique(vec.begin(), vec.end()), vec.end()); for (int i = 1; i <= n; ++i) a[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin(); m = read(); for (int i = 1; i <= m; ++i) qry[i].l = read(), qry[i].r = read(), qry[i].id = i; sort(qry + 1, qry + 1 + m); fill(BFlpos, BFlpos + vec.size(), n + 1); fill(BFrpos, BFrpos + vec.size(), 0); for (int i = 1, pos = 1; i <= tot; ++i) { fill(lpos, lpos + vec.size(), n + 1); fill(rpos, rpos + vec.size(), 0); int result = 0; for (int r = R[i], l = R[i] + 1; pos <= m && bid[qry[pos].l] == i; ++pos) { if (bid[qry[pos].r] == i) { ans[qry[pos].id] = BruteForce(qry[pos].l, qry[pos].r); continue; } while (r < qry[pos].r) { ++r; lpos[a[r]] = min(lpos[a[r]], r), rpos[a[r]] = max(rpos[a[r]], r); result = max(result, rpos[a[r]] - lpos[a[r]]); } int nowresult = result; while (l > qry[pos].l) { --l; BFlpos[a[l]] = min(BFlpos[a[l]], l), BFrpos[a[l]] = max(BFrpos[a[l]], l); nowresult = max(nowresult, max(rpos[a[l]], BFrpos[a[l]]) - min(lpos[a[l]], BFlpos[a[l]])); } ans[qry[pos].id] = nowresult; while (l <= R[i]) BFlpos[a[l]] = n + 1, BFrpos[a[l]] = 0, ++l; } } for (int i = 1; i <= m; ++i) printf("%d\n", ans[i]); return 0; }
二维莫队
每个状态有四个方向可以扩展,每次移动指针要操作一行或者一列的数,具体实现方式与普通的一维莫队类似。
取块长 B=n×q−0.25 ,总时间复杂度为 O(n2×q0.75) 。
P1527 [国家集训队] 矩阵乘法
q 次询问 n×n 矩阵中一个子矩阵的第 k 小数。
n≤500,q≤60000
先离散化,用莫队维护出矩形中出现的数,再值域分块即可。
这题好像二维莫队过不去,只有 60 分。
#include <bits/stdc++.h> using namespace std; const int N = 5e2 + 7, M = 6e4 + 7; int bid[N]; struct Query { int u, l, d, r, k, id; inline bool operator < (const Query &rhs) const { if (bid[u] == bid[rhs.u]) { if (bid[l] == bid[rhs.l]) { if (bid[r] == bid[rhs.r]) return bid[r] & 1 ? d < rhs.d : d > rhs.d; else return bid[l] & 1 ? r < rhs.r : r > rhs.r; } else return bid[l] < bid[rhs.l]; } else return bid[u] < bid[rhs.u]; } } qry[M]; vector<int> vec; int a[N][N], cnt[N * N], s[N], ans[M]; int n, m, block, vblock; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline void add1(int l, int r, int x) { for (int i = l; i <= r; ++i) ++cnt[a[x][i]], ++s[a[x][i] / vblock]; } inline void add2(int u, int d, int x) { for (int i = u; i <= d; ++i) ++cnt[a[i][x]], ++s[a[i][x] / vblock]; } inline void del1(int l, int r, int x) { for (int i = l; i <= r; ++i) --cnt[a[x][i]], --s[a[x][i] / vblock]; } inline void del2(int u, int d, int x) { for (int i = u; i <= d; ++i) --cnt[a[i][x]], --s[a[i][x] / vblock]; } inline int query(int k) { int cur = 0; while (k > s[cur]) k -= s[cur++]; cur *= vblock; while (k > cnt[cur]) k -= cnt[cur++]; return cur; } signed main() { n = read(), m = read(), block = n / pow(m, 0.25) / 3 + 1; for (int i = 1; i <= n; ++i) bid[i] = (i - 1) / block + 1; for (int i = 1; i <= n; ++i) for (int j = 1; j <= n; ++j) vec.emplace_back(a[i][j] = read()); sort(vec.begin(), vec.end()); vec.erase(unique(vec.begin(), vec.end()), vec.end()); vblock = sqrt(vec.size()); for (int i = 1; i <= n; ++i) for (int j = 1; j <= n; ++j) a[i][j] = lower_bound(vec.begin(), vec.end(), a[i][j]) - vec.begin(); for (int i = 1; i <= m; ++i) qry[i].u = read(), qry[i].l = read(), qry[i].d = read(), qry[i].r = read(), qry[i].k = read(), qry[i].id = i; sort(qry + 1, qry + 1 + m); for (int i = 1, u = 1, l = 1, d = 0, r = 0; i <= m; ++i) { while (u > qry[i].u) add1(l, r, --u); while (l > qry[i].l) add2(u, d, --l); while (d < qry[i].d) add1(l, r, ++d); while (r < qry[i].r) add2(u, d, ++r); while (u < qry[i].u) del1(l, r, u++); while (l < qry[i].l) del2(u, d, l++); while (d > qry[i].d) del1(l, r, d--); while (r > qry[i].r) del2(u, d, r--); ans[qry[i].id] = vec[query(qry[i].k)]; } for (int i = 1; i <= m; ++i) printf("%d\n", ans[i]); return 0; }
莫队二次离线
适用范围:
- 可以莫队。
- 一个数对答案的贡献与区间中别的数有关,移动端点的更新时间不是 O(1) 。
考虑把莫队移动的这些端点也都离线下来预处理,从而进一步优化。
因为莫队时一次离线,最后处理时又一次离线,故称为莫队二次离线。
假设更新答案的时间复杂度为 O(k) ,则使用二次离线莫队可以将时间复杂度从 O(nk√n) 降到 O(nk+n√n) 。
实现
考虑端点移动对答案的影响,以 [l,r]→[l,r+k] 为例,设 x 对区间 [l,r] 的贡献为 f(x,[l,r]) ,则考虑求 ∀x∈[r+1,r+k] 时 ∑f(x,[l,x−1]) 的值。可以差分:
对于 f(x,[1,x−1]) 可以预处理得出,对于 f(x,[1,l−1]) 可以在每个 l−1 的位置记录一下需要计算哪些数的贡献,打标记的时候可以只标记左右端点,最后一起处理。
注意最后处理时须保证查询复杂度为 O(1) ,时间复杂度才是 O(nk+n√n) 。
应用
P4887 【模板】莫队二次离线(第十四分块(前体))
m 次查询,每次给出 l,r,k ,求满足 l≤i<j≤r,popcount(ai⊕aj)=k 的二元组 (i,j) 的个数。
n,m≤105
模板,下给出参考代码。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 1e5 + 7, V = 1 << 14 | 1; struct Range { int l, r, id; inline Range() {} inline Range(int _l, int _r, int _id) : l(_l), r(_r), id(_id) {} }; struct Query { int l, r, id, bid; inline bool operator < (const Query &rhs) const { return bid == rhs.bid ? r < rhs.r : l < rhs.l; } } qry[N]; vector<Range> ask[N]; vector<int> AcceptedNumber; ll res[N], ans[N]; int a[N], f[N], g[N]; int n, m, k, block; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } signed main() { n = read(), m = read(), k = read(), block = sqrt(n); for (int i = 1; i <= n; ++i) a[i] = read(); for (int i = 0; i < V; ++i) if (__builtin_popcount(i) == k) AcceptedNumber.emplace_back(i); for (int i = 1; i <= n; ++i) { f[i] = g[a[i]]; // f[i] 表示 a[i] 对 [1, i - 1] 的贡献 for (int x : AcceptedNumber) ++g[x ^ a[i]]; } for (int i = 1; i <= m; ++i) qry[i].l = read(), qry[i].r = read(), qry[i].id = i, qry[i].bid = qry[i].l / block; sort(qry + 1, qry + 1 + m); for (int i = 1, l = 1, r = 0; i <= m; ++i) { if (l > qry[i].l) ask[r].emplace_back(qry[i].l, l - 1, i); while (l > qry[i].l) res[i] -= f[--l]; if (r < qry[i].r) ask[l - 1].emplace_back(r + 1, qry[i].r, -i); while (r < qry[i].r) res[i] += f[++r]; if (l < qry[i].l) ask[r].emplace_back(l, qry[i].l - 1, -i); while (l < qry[i].l) res[i] += f[l++]; if (r > qry[i].r) ask[l - 1].emplace_back(qry[i].r + 1, r, i); while (r > qry[i].r) res[i] -= f[r--]; } memset(g, 0, sizeof(g)); for (int i = 1; i <= n; ++i) { for (int x : AcceptedNumber) ++g[x ^ a[i]]; for (Range it : ask[i]) for (int j = it.l; j <= it.r; ++j) if (it.id > 0) res[it.id] += g[a[j]] - (!k && j <= i); else res[-it.id] -= g[a[j]] - (!k && j <= i); } for (int i = 1; i <= m; ++i) res[i] += res[i - 1], ans[qry[i].id] += res[i]; for (int i = 1; i <= m; ++i) printf("%lld\n", ans[i]); return 0; }
[Ynoi2019 模拟赛] Yuno loves sqrt technology II
给你一个长为 n 的序列 a,m 次询问区间的逆序对数。
n≤105
首先直接莫队是 O(n√nlogn) 的,然后二次离线就可以做到 O(n√n+nlogn) 了。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 1e5 + 7; struct Range { int l, r, id; inline Range() {} inline Range(int _l, int _r, int _id) : l(_l), r(_r), id(_id) {} }; struct Query { int l, r, id, bid; inline bool operator < (const Query &rhs) const { return bid == rhs.bid ? r < rhs.r : l < rhs.l; } } qry[N]; vector<Range> askl[N], askr[N]; ll pre[N], suf[N], res[N], ans[N]; int a[N]; int n, m, block; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } namespace FK { int Sum[N], sum[N], bel[N]; inline void prework() { for (int i = 0; i <= n; ++i) bel[i] = i / block + 1, Sum[i] = sum[i] = 0; } inline void update(int x) { for (int i = bel[x] + 1; i <= bel[n]; ++i) ++Sum[i]; for (int pos = bel[x]; x <= n && bel[x] == pos; ++x) ++sum[x]; } inline int query(int x) { return Sum[bel[x]] + sum[x]; } } // namespace FK signed main() { n = read(), m = read(), block = sqrt(n) + 1; vector<int> vec; for (int i = 1; i <= n; ++i) vec.emplace_back(a[i] = read()); sort(vec.begin(), vec.end()); vec.erase(unique(vec.begin(), vec.end()), vec.end()); for (int i = 1; i <= n; ++i) a[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin() + 1; FK::prework(); for (int i = 1; i <= n; ++i) pre[i] = pre[i - 1] + i - 1 - FK::query(a[i]), FK::update(a[i]); FK::prework(); for (int i = n; i; --i) suf[i] = suf[i + 1] + FK::query(a[i] - 1), FK::update(a[i]); for (int i = 1; i <= m; ++i) qry[i].l = read(), qry[i].r = read(), qry[i].id = i, qry[i].bid = qry[i].l / block; sort(qry + 1, qry + 1 + m); for (int i = 1, l = 1, r = 0; i <= m; ++i) { res[i] += (suf[qry[i].l] - suf[l]); res[i] += (pre[qry[i].r] - pre[r]); if (l > qry[i].l) askr[qry[i].r + 1].emplace_back(qry[i].l, l - 1, -i); if (r < qry[i].r) askl[l - 1].emplace_back(r + 1, qry[i].r, -i); if (l < qry[i].l) askr[qry[i].r + 1].emplace_back(l, qry[i].l - 1, i); if (r > qry[i].r) askl[l - 1].emplace_back(qry[i].r + 1, r, i); l = qry[i].l, r = qry[i].r; } FK::prework(); for (int i = 1; i <= n; ++i) { FK::update(a[i]); for (Range it : askl[i]) for (int j = it.l; j <= it.r; ++j) if (it.id > 0) res[it.id] += i - FK::query(a[j]); else res[-it.id] -= i - FK::query(a[j]); } FK::prework(); for (int i = n; i; --i) { FK::update(a[i]); for (Range it : askr[i]) for (int j = it.l; j <= it.r; ++j) if (it.id > 0) res[it.id] += FK::query(a[j] - 1); else res[-it.id] -= FK::query(a[j] - 1); } for (int i = 1; i <= m; ++i) res[i] += res[i - 1], ans[qry[i].id] = res[i]; for (int i = 1; i <= m; ++i) printf("%lld\n", ans[i]); return 0; }
P5501 [LnOI2019] 来者不拒,去者不追
m 次询问区间内所有数的价值和,一个数的价值定义为它在区间内的排名乘本身。
n,m≤5×105,ai≤105
考虑加入 x 这个数的贡献:
- 对于所有大于 x 的数 y ,贡献全部增加了 y 。
- 对于 x 本身,贡献就是 x×t ,t 为 [l,r] 中比 x 小的数加 1 。
于是莫队二次离线时维护大于 x 的数的总和和小于 x 的数的数量即可。
#include <bits/stdc++.h> typedef long long ll; using namespace std; const int N = 5e5 + 7, V = 1e5 + 7; struct Range { int l, r, id; inline Range() {} inline Range(int _l, int _r, int _id) : l(_l), r(_r), id(_id) {} }; struct Query { int l, r, id, bid; inline bool operator < (const Query &rhs) const { return bid == rhs.bid ? r < rhs.r : l < rhs.l; } } qry[N]; vector<Range> ask[N]; ll s[N], pre[N], res[N], ans[N]; int a[N]; int n, m, block; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } namespace FK { const int block = sqrt(V); ll Sum[V], sum[V]; int Cnt[V], cnt[V]; ll total; inline void clear() { memset(Sum, 0, sizeof(Sum)); memset(sum, 0, sizeof(sum)); memset(Cnt, 0, sizeof(Cnt)); memset(cnt, 0, sizeof(cnt)); total = 0; } inline void update(int x) { total += x; for (int i = x / block; i <= (V - 1) / block; ++i) Sum[i] += x, ++Cnt[i]; for (int i = x; i < V && i / block == x / block; ++i) sum[i] += x, ++cnt[i]; } inline ll querysum(int x) { return (x / block ? Sum[x / block - 1] : 0) + sum[x]; } inline int querycnt(int x) { return (x / block ? Cnt[x / block - 1] : 0) + cnt[x]; } inline ll query(int x) { return total - querysum(x) + 1ll * x * querycnt(x - 1); } } // namespace FK signed main() { n = read(), m = read(), block = sqrt(n) + 1; for (int i = 1; i <= n; ++i) s[i] = s[i - 1] + (a[i] = read()); FK::clear(); for (int i = 1; i <= n; ++i) pre[i] = pre[i - 1] + FK::query(a[i]), FK::update(a[i]); for (int i = 1; i <= m; ++i) qry[i].l = read(), qry[i].r = read(), qry[i].id = i, qry[i].bid = qry[i].l / block; sort(qry + 1, qry + 1 + m); for (int i = 1, l = 1, r = 0; i <= m; ++i) { if (l > qry[i].l) { ask[r].emplace_back(qry[i].l, l - 1, i); res[i] -= pre[l - 1] - pre[qry[i].l - 1], l = qry[i].l; } if (r < qry[i].r) { ask[l - 1].emplace_back(r + 1, qry[i].r, -i); res[i] += pre[qry[i].r] - pre[r], r = qry[i].r; } if (l < qry[i].l) { ask[r].emplace_back(l, qry[i].l - 1, -i); res[i] += pre[qry[i].l - 1] - pre[l - 1], l = qry[i].l; } if (r > qry[i].r) { ask[l - 1].emplace_back(qry[i].r + 1, r, i); res[i] -= pre[r] - pre[qry[i].r], r = qry[i].r; } } FK::clear(); for (int i = 1; i <= n; ++i) { FK::update(a[i]); for (Range it : ask[i]) for (int j = it.l; j <= it.r; ++j) if (it.id > 0) res[it.id] += FK::query(a[j]); else res[-it.id] -= FK::query(a[j]); } for (int i = 1; i <= m; ++i) res[i] += res[i - 1], ans[qry[i].id] = res[i] + s[qry[i].r] - s[qry[i].l - 1]; for (int i = 1; i <= m; ++i) printf("%lld\n", ans[i]); return 0; }
莫队配合 bitset
P4688 [Ynoi2016] 掉进兔子洞
m 次询问,每次询问三个区间,把三个区间中同时出现的数一个个删掉,求最后三个区间剩下的数的个数和,询问独立。
n,m≤105,ai≤109
答案为 ∑3i=1(ri−li+1)−3×k,其中 k 为三段区间内共有的数的个数,问题转化为求 k 。
考虑用莫队维护查询的区间内每个数的个数。对于一次询问,我们将其拆成传统莫队的三个询问:(l1,r1),(l2,r2),(l3,r3) ,分别进行处理。我们要得到 k,需要得到这三个询问中有哪些数、分别几个,然后取它们的交集即可。
注意一个数可能重复出现多次。我们考虑先将初始 a 数组进行离散化,只记录 a 数组每一个值对应排序好的数组的编号即可。这样,若干个相同的数便会留出若干个空着的编号,它们代表同一个值。然后使用这个离散化的对应关系用 bitset
存即可。
但是 105 的范围 bitset
存不下,可以把输入的查询分为若干组,每组 2×104 个数,对于每一组分别进行处理即可。
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7, M = 2e4 + 7; struct Query { int l, r, id, bid; inline bool operator < (const Query &rhs) const { return bid == rhs.bid ? (bid & 1 ? r > rhs.r : r < rhs.r) : l < rhs.l; } } qry[N]; bitset<N> res[M], result; int a[N], cnt[N], ans[M]; int n, m, block; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline void add(int x) { result.set(a[x] + (cnt[a[x]]++)); } inline void del(int x) { result.reset(a[x] + (--cnt[a[x]])); } signed main() { n = read(), m = read(), block = sqrt(n); vector<int> vec; for (int i = 1; i <= n; ++i) vec.emplace_back(a[i] = read()); sort(vec.begin(), vec.end()); for (int i = 1; i <= n; ++i) a[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin(); for (int task = 1; task <= 5; ++task) { int q = min(m, 20000), qcnt = 0; for (int i = 1; i <= q; ++i) { ans[i] = 0, res[i].set(); for (int j = 1; j <= 3; ++j) { qry[++qcnt].l = read(), qry[qcnt].r = read(); qry[qcnt].id = i, qry[qcnt].bid = qry[qcnt].l / block; ans[i] += qry[qcnt].r - qry[qcnt].l + 1; } } sort(qry + 1, qry + 1 + qcnt); memset(cnt, 0, sizeof(cnt)); result.reset(); for (int i = 1, l = 1, r = 0; i <= qcnt; ++i) { while (l > qry[i].l) add(--l); while (r < qry[i].r) add(++r); while (l < qry[i].l) del(l++); while (r > qry[i].r) del(r--); res[qry[i].id] &= result; } for (int i = 1; i <= q; ++i) printf("%d\n", ans[i] - (int)res[i].count() * 3); m = max(m - 20000, 0); } return 0; }
m 次询问,每次询问区间内是否能找出两个数使得它们的差/和/积/商为 x 。
n,m,ai≤105
加减维护一个 bitset
就好了,单次询问复杂度为 O(nω) 。
积直接暴力枚举因数,单次询问复杂度为 O(√n) 。
对于商,分类讨论:
- 如果 x≥√n ,那么可以暴力枚举商,然后判断有没有出现即可。因为这个商 ≤√n ,所以复杂度是正确的。
- 如果 x<√n ,可以预处理出 x∈[1,√n) 的答案。对于每个 x 遍历一遍序列,找出每个 1≤i≤n 的离 i 最近且 ≤i 的 tpi ,满足 ai 和 atpi 的商为 x ,于是每个询问的答案为 l≤tpr 。这一部分的时间复杂度为 O(n√n) 。
总时间复杂度为 O(n2ω+n√n) 。
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7; struct Query { int op, l, r, k, id, bid; inline bool operator < (const Query &rhs) const { return bid == rhs.bid ? (bid & 1 ? r > rhs.r : r < rhs.r) : l < rhs.l; } } qry[N]; struct Ask { int l, r, id; }; bitset<N> result1, result2; // i in result1, N - i in result2 vector<Ask> ask[N]; int a[N], cnt[N], pre[N], tp[N]; bool ans[N]; int n, m, block, cntq; template <class T = int> inline T read() { char c = getchar(); bool sign = (c == '-'); while (c < '0' || c > '9') c = getchar(), sign |= (c == '-'); T x = 0; while ('0' <= c && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return sign ? (~x + 1) : x; } inline void add(int x) { if (!cnt[a[x]]) result1.set(a[x]), result2.set(N - a[x]); ++cnt[a[x]]; } inline void del(int x) { --cnt[a[x]]; if (!cnt[a[x]]) result1.reset(a[x]), result2.reset(N - a[x]); } signed main() { n = read(), m = read(), block = sqrt(n) + 1; for (int i = 1; i <= n; ++i) a[i] = read(); for (int i = 1; i <= m; ++i) { int op = read(), l = read(), r = read(), k = read(); if (op == 4 && k <= block) ask[k].emplace_back((Ask) {l, r, i}); else qry[++cntq] = (Query){op, l, r, k, i, l / block}; } sort(qry + 1, qry + 1 + cntq); for (int i = 1, l = 1, r = 0; i <= cntq; ++i) { while (l > qry[i].l) add(--l); while (r < qry[i].r) add(++r); while (l < qry[i].l) del(l++); while (r > qry[i].r) del(r--); if (qry[i].op == 1) ans[qry[i].id] = (result1 & (result1 << qry[i].k)).any(); else if (qry[i].op == 2) { ans[qry[i].id] = (result1 & (result2 >> (N - qry[i].k))).any(); } else if (qry[i].op == 3) { for (int j = 1; j * j <= qry[i].k; ++j) if (!(qry[i].k % j) && result1.test(j) && result1.test(qry[i].k / j)) { ans[qry[i].id] = true; break; } } else { for (int j = 1; j * qry[i].k < N; ++j) if (result1.test(j) && result1.test(j * qry[i].k)) { ans[qry[i].id] = true; break; } } } for (int i = 1; i <= block; ++i) { if (ask[i].empty()) continue; memset(pre, 0, sizeof(pre)); memset(tp, 0, sizeof(tp)); for (int j = 1, pos = 0; j <= n; ++j) { pre[a[j]] = j; if (a[j] * i < N) pos = max(pos, pre[a[j] * i]); if (!(a[j] % i)) pos = max(pos, pre[a[j] / i]); tp[j] = pos; } for (Ask it : ask[i]) ans[it.id] = (it.l <= tp[it.r]); } for (int i = 1; i <= m; ++i) puts(ans[i] ? "yuno" : "yumi"); return 0; }
m 次询问区间内元素组成最长的首项 <b 公差为 b ( b 每次询问给出)的等差数列的长度。
n,m,ai≤105
当 b>B 时,用莫队提取出一段区间中代表数字出现情况的 bitset
,然后把这个大 bitset
从 0 开始分裂成一些长度为 b 的小 bitset
。显然,如果将分裂出来的这些 bitset
全部与起来,第一次全为 0 时的小 bitset
的下标即为本次询问的答案。
当 b≤B 时,考虑对每个 b 的剩余类分别处理,原问题可转化为在长为 nB 的序列上,询问 qB 次区间 mex 。线段树上二分可以做到 O(nBlogn) 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】