【学习笔记】莫队
莫队是一种离线算法,用于求区间信息。
普通莫队
假设已知 \([l,r]\) 的答案,可以 \(O(1)\) 求出 \([l+1,r],[l-1,r],[l,r+1],[l,r-1]\) 的答案。莫队的基本流程就是按一定顺序处理所有询问,每次通过上一个询问拓展/缩小范围得到当前答案。基本框架:
while (l > q[i].l) add(--l);
while (r < q[i].r) add(++r);
while (l < q[i].l) del(l+);
while (r > q[i].r) del(r--);
但注意,增减 \(l\) 和 \(r\) 的顺序最好是先扩大范围,再减小,否则可能会出现减去原先没有的的问题。
容易想到根据左端点排序。但这样的复杂度是 \(O(n+nq)\) 的,其中左端点移动复杂度为 \(O(n)\),右端点为 \(O(nq)\)。差距有点大,不如均衡一下,于是想到分块处理。
设块长为 \(S\),按第一关键字为左端点的块的编号从小到大,第二关键字为右端点从大到小进行排序。
那么对于左端点,每个询问移动的距离不会超过 \(O(S)\),总复杂度为 \(O(qS)\);对于右端点,左端点在一个块内时均摊为 \(O(n)\),总复杂度为 \(O(\frac{n^2}{S})\)。因此,总复杂度为 \(O(qS+\frac{n^2}{S})\)。
块长根据 \(q\) 与 \(n\) 的大小而定。当 \(q\) 与 \(n\) 同阶时,取 \(S=\sqrt n\) 复杂度为 \(O(n\sqrt n)\) 最优。
奇偶性优化:在跨越块时右端点需要重新扫一遍,可以在奇数块按右端点从小到大、偶数块从大到小排序来避免。
例题
P1494 [国家集训队] 小 Z 的袜子
先推一下式子:设 \([l,r]\) 内颜色 \(i\) 的个数为 \(cnt_i\),记 \(len=r-l+1\),那么答案则为:
于是用如上所说的莫队维护 \(\sum_{i=1}^ncnt_i^2\) 即可。这题 \(n,m\) 同阶,因此取 \(S=\sqrt n\)。
#include <bits/stdc++.h>
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e4 + 5;
inline int read() {
int q = 0; char ch = getchar();
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') q = q * 10 + ch - '0', ch = getchar();
return q;
}
struct node { int l, r, id; }q[N];
int sq, a[N], s[N], ans1[N], ans2[N], sum;
inline bool cmp(node x, node y) {
int tx = (x.l - 1) / sq, ty = (y.l - 1) / sq;
if (tx != ty) return tx < ty;
else return ((tx & 1) ? x.r < y.r : x.r > y.r);
}
inline void add(int x, int w) {
sum -= s[a[x]] * s[a[x]], s[a[x]] += w, sum += s[a[x]] * s[a[x]];
}
signed main() {
int n = read(), m = read(); sq = sqrt(n);
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= m; i++)
q[i].l = read(), q[i].r = read(), q[i].id = i;
sort(q + 1, q + 1 + m, cmp); q[0].l = 1;
for (int i = 1; i <= m; i++) {
int l = q[i - 1].l, r = q[i - 1].r;
while (l > q[i].l) add(--l, 1);
while (r < q[i].r) add(++r, 1);
while (l < q[i].l) add(l++, -1);
while (r > q[i].r) add(r--, -1);
int len = q[i].r - q[i].l + 1;
if (len == 1) ans2[q[i].id] = 1;
else {
ans1[q[i].id] = sum - len;
ans2[q[i].id] = len * (len - 1);
int g = __gcd(ans1[q[i].id], ans2[q[i].id]);
ans1[q[i].id] /= g, ans2[q[i].id] /= g;
}
}
for (int i = 1; i <= m; i++)
printf("%d/%d\n", ans1[i], ans2[i]);
return 0;
}
P1997 faebdc 的烦恼
重点在于 add 和 del 的实现。
具体来说,记 \(s_i\) 表示 \(i\) 的出现次数,\(cnt_j\) 表示 \(s_i=j\) 的 \(i\) 的个数。每次操作时更新 \(s\) 与 \(cnt\),若增加完的值大于 \(max\) 就更新,若删除完 \(cnt_{max}=0\) 了就将 \(max\leftarrow max-1\)。
void add(int x) {
cnt[s[a[x]]]--, cnt[++s[a[x]]]++;
maxx = max(maxx, s[a[x]]);
}
void del(int x) {
cnt[s[a[x]]]--, cnt[--s[a[x]]]++;
if (!cnt[s[a[x]] + 1] && maxx == s[a[x]] + 1) maxx--;
}
P4688 [Ynoi Easy Round 2016] 掉进兔子洞
先考虑单次查询怎么做。题目求的权值可以转化为:\(\sum len-3\times \sum_x \min\{cnt_x\}\)。
如果同种数只看做一个的话,可以用 bitset 轻松维护。一个 trick 是将 \(a\) 离散化为小于等于 \(a\) 的个数,然后将第 \(x\) 个加入的 \(y\) 的权值当做 \(y-x\) 放到 bitset 里。
于是就可以上莫队维护了(把一个询问拆成三个区间询问)!
需要注意的是,\(O(nm)\) 的 bitset 空间开不下,需要将所有询问分为常数组处理。
#include <bits/stdc++.h>
#define fi first
#define se second
using namespace std;
const int N = 1e5 + 5;
struct node {
int w, id;
bool operator <(node x) const {
return w < x.w;
}
} a[N];
int n, cnt[N], b[N], len[N], w[N];
struct query {
int l, r, id;
bool operator <(query x) const {
if (b[l] != b[x.l]) return l < x.l;
else return r < x.r;
}
} Q[N];
bitset <N> ans[N / 3], now;
inline void add(int x) {
x = w[x], cnt[x]++, now[x - cnt[x]] = true;
}
inline void del(int x) {
x = w[x], now[x - cnt[x]] = false, cnt[x]--;
}
inline void solve(int m) {
if (!m) return; now.reset();
memset(cnt, 0, sizeof(cnt));
memset(len, 0, sizeof(len));
int B = sqrt(n), ts = 0;
for (int i = 1; i <= n; i++)
b[i] = (i - 1) / B + 1;
for (int i = 1; i <= m; i++) {
ans[i].set();
for (int j = 0; j < 3; j++) {
ts++, Q[ts].id = i;
cin >> Q[ts].l >> Q[ts].r;
len[i] += Q[ts].r - Q[ts].l + 1;
}
}
sort(Q + 1, Q + 1 + ts); int l = 1, r = 0;
for (int i = 1; i <= ts; i++) {
while (r < Q[i].r) add(++r);
while (l > Q[i].l) add(--l);
while (r > Q[i].r) del(r--);
while (l < Q[i].l) del(l++);
ans[Q[i].id] &= now;
}
for (int i = 1; i <= m; i++)
cout << len[i] - ans[i].count() * 3 << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int m; cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i].w, a[i].id = i;
sort(a + 1, a + 1 + n);
for (int i = 1, j = 1; i <= n; i++) {
while (j < n && a[j + 1].w == a[j].w) j++;
for (int k = i; k <= j; k++) w[a[k].id] = j;
i = j, j++;
}
solve(m / 3), solve((m - m / 3) / 2);
solve((m - m / 3 + 1) / 2); // 这里分为三组
return 0;
}
练习
P4462 [CQOI2018] 异或序列 类似 P1494,有一些细节。
P3730 曼哈顿交易 莫队+值域分块。
P4396 [AHOI2013] 作业 莫队+值域分块/树状数组。
回滚莫队
普通莫队需要信息可以拓展和删除,当不能支持其中一种操作时,就可以使用回滚莫队,更多情况下是不能支持删除(如区间 \(\max\))。下文讲的不删除莫队。
还是和普通莫队一样,将区间按照第一关键字为左端点所在块、第二关键字为右端点升序排序。
如何在暴力基础上优化?考虑维护 \([L,R]\) 表示处理询问的公共部分。
每当左端点进入一个新块 \(x\),令 \(L=l_{x+1},R=L-1\)。\(R\) 可以直接拓展,\(L\) 则需要撤销完成。
具体的,先将 \(L,R\) 扩展,需要记录因 \(L\) 的扩展答案的变化情况,计算答案,然后将 \(L\) 扩展的影响撤回。撤回最多撤回块长次,因此复杂度可以得到保证。
易错点:
- 回滚莫队无法完成询问左右端点在同一个块内的情况,要直接暴力完成
- 在进入新块时,记得把所有信息都清空,这个步骤最好在判暴力前
- 要求询问 \(r\) 递增,因此不能使用奇偶性优化。
- 注意暴力及 \(L\) 扩展的答案不能继承,\(R\) 扩展的必须继承(新块要清空)
例题
P5906 【模板】回滚莫队&不删除莫队
板子,“撤销”可以通过用另一个数组实现。复杂度 \(O((n+m)\sqrt n)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
struct query { int l, r, id; } qry[N];
int a[N], lsh[N], ans[N];
int b[N], bl[N], br[N], mn[N], mx[N], tmp[N];
bool cmp(query x, query y) {
if (b[x.l] != b[y.l]) return x.l < y.l;
else return x.r < y.r;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n; cin >> n; int B = sqrt(n);
for (int i = 1; i <= n; i++)
cin >> a[i], lsh[i] = a[i];
sort(lsh + 1, lsh + 1 + n);
int ts = unique(lsh + 1, lsh + 1 + n) - lsh - 1;
for (int i = 1; i <= n; i++)
a[i] = lower_bound(lsh, lsh + ts + 1, a[i]) - lsh;
for (int i = 1; i <= n; i++) {
b[i] = b[i - 1] + (i % B == 1);
if (!bl[b[i]]) bl[b[i]] = i;
br[b[i]] = i;
}
int m; cin >> m;
for (int i = 1; i <= m; i++)
cin >> qry[i].l >> qry[i].r, qry[i].id = i;
sort(qry + 1, qry + 1 + m, cmp);
for (int i = 1, L, R, res = 0; i <= m; i++) {
int l = qry[i].l, r = qry[i].r, tres = 0;
// res 是要继承的,tres 是不要继承的
if (b[l] != b[qry[i - 1].l]) {
for (int j = 1; j <= ts; j++)
mn[j] = mx[j] = 0;
L = br[b[l]] + 1, R = L - 1, res = 0;
}
if (b[l] == b[r]) {
for (int j = l; j <= r; j++) {
if (!mn[a[j]]) mn[a[j]] = j;
tres = max(tres, (mx[a[j]] = j) - mn[a[j]]);
}
for (int j = l; j <= r; j++) // 不能全清了,复杂度不对
mn[a[j]] = mx[a[j]] = 0;
} else {
while (R < r) {
if (!mn[a[++R]]) mn[a[R]] = R;
res = max(res, (mx[a[R]] = R) - mn[a[R]]);
}
for (int j = L - 1; j >= l; j--) {
if (!tmp[a[j]]) tmp[a[j]] = j;
tres = max(tres, max(tmp[a[j]], mx[a[j]]) - j);
// 有可能后面也有贡献要取个 max
}
for (int j = L - 1; j >= l; j--) tmp[a[j]] = 0;
// tmp 数组也要记得清空
}
ans[qry[i].id] = max(res, tres);
}
for (int i = 1; i <= m; i++)
cout << ans[i] << '\n';
return 0;
}
P8078 [WC2022] 秃子酋长
这题是不支持拓展的例子。
容易想到 \(O(n\sqrt n\log n)\) 的做法:用 set 维护前驱后继,然后做普通莫队。
如果用链表呢?删除是简单的,但是加入可能退化为 \(O(n)\)。实际上,虽不能加入,但是可以撤销,于是就可以使用回滚莫队。
类似的,需要维护 \(L,R\)。这里的 \(L,R\) 在新进入一个块 \(x\) 时初始为 \(l_x\) 和 \(n\)。同一个块内 \(r\) 降序排序,那么 \(R\) 不断减小,\(L\) 移动范围不超过 \(\sqrt n\)。复杂度 \(O(n\sqrt n)\)。
和上题不一样的小细节:这题不用特殊处理一个块内的情况。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e5 + 5;
struct List {
ll ans; int l[N], r[N];
stack <int> q;
List() {
ans = 0;
memset(l, 0, sizeof(l));
memset(r, 0, sizeof(r));
while (!q.empty()) q.pop();
}
inline void init(int a[], int n) {
for (int i = 2; i <= n; i++) {
r[a[i - 1]] = a[i], l[a[i]] = a[i - 1];
ans += abs(a[i] - a[i - 1]);
}
}
inline void del(int x, int op) {
if (op) q.push(x);
if (l[x] && r[x]) ans += abs(l[x] - r[x]);
if (l[x]) ans -= abs(l[x] - x);
if (r[x]) ans -= abs(r[x] - x);
l[r[x]] = l[x], r[l[x]] = r[x];
}
inline void undo() {
while (!q.empty()) {
int t = q.top(); q.pop();
if (l[t] && r[t]) ans -= abs(l[t] - r[t]);
if (l[t]) ans += abs(l[t] - t);
if (r[t]) ans += abs(r[t] - t);
r[l[t]] = l[r[t]] = t;
}
}
List& operator =(const List &a) {
if (this == &a) return *this;
ans = a.ans, q = a.q;
memcpy(l, a.l, sizeof(a.l));
memcpy(r, a.r, sizeof(a.r));
return *this;
}
};
int a[N], id[N], b[N], bl[N];
ll ans[N];
inline bool cmp(int x, int y) {
return a[x] < a[y];
}
struct qry {
int l, r, id;
bool operator <(const qry& x) const {
if (b[l] != b[x.l]) return l < x.l;
else return r > x.r;
}
} Q[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m; int len = sqrt(n);
for (int i = 1; i <= n; i++)
cin >> a[i], id[i] = i;
sort(id + 1, id + 1 + n, cmp);
List all, now; all.init(id, n); now = all;
for (int i = 1; i <= n; i++) {
b[i] = (i - 1) / len + 1;
if (!bl[b[i]]) bl[b[i]] = i;
}
for (int i = 1; i <= m; i++)
cin >> Q[i].l >> Q[i].r, Q[i].id = i;
sort(Q + 1, Q + 1 + m);
for (int i = 1, L = 1, R = n; i <= m; i++) {
int l = Q[i].l, r = Q[i].r;
if (b[Q[i - 1].l] != b[l] && i > 1) {
while (L < bl[b[l]]) all.del(L++, 0);
R = n, now = all;
}
while (R > r) now.del(R--, 0);
for (int j = L; j < l; j++) now.del(j, 1);
ans[Q[i].id] = now.ans; now.undo();
}
for (int i = 1; i <= m; i++)
cout << ans[i] << '\n';
return 0;
}
练习
- AT_joisc2014_c 歴史の研究:回滚莫队板子。
带修莫队
上述莫队都是无法支持修改的,如果题目有单点修改怎么办?
不妨考虑一个暴力的方法:把原来的 \(l,r\) 移动变成 \(l,r,t\)(\(t\) 为时间)的移动。那么对于在移动 \(t\) 的过程中,便可以把会影响到当前查询的修改加进去。
和原来一样,还是采用分块的思想,每次可以把 \(l,r,t\) 中的一个左移或右移。
先说排序方法:升序,第一关键字 \(l\) 所在块,第二关键字 \(r\) 所在块,第三关键字 \(t\)。此时分块的块长取什么最优?记块长为 \(S\),序列长 \(n\),修改个数 \(m\),询问个数 \(q\),对于 \(l,r,t\) 分别分析:
- \(l\) 的移动:\(O(qS)\)。
- 不跨块时每次移动 \(O(S)\),总移动 \(O(qS)\)。
- 跨块总移动 \(n\),总移动 \(O(qS)\)。
- \(r\) 的移动:\(O(qS+\frac{n^2}{S})\)
- \(l\) 不跨块时每次移动 \(O(S)\),总移动 \(O(qS)\)。
- \(l\) 跨块时最多移动 \(O(n)\),共 \(O(\frac{n}{S})\) 个块,总移动 \(O(\frac{n^2}{S})\)。
- \(t\) 的移动:\(O(\frac{n^3}{S^2})\)
- \(l,r\) 的块的组合共 \(\min\{O(q),O(\frac{n^2}{S^2})\}\) 种,每种最多 \(O(m)\) 次 \(\min\{O(mq),O(\frac{mn^2}{S^2})\}\)。
- \(O(mq)\) 的复杂度基本上不能接受,因此要求取到后面那种,即:\(S>\frac{n}{\sqrt q}\)。
因此总的复杂度为 \(O(qS+\frac{n^2}{S}+\frac{mn^2}{S^2})\)。
\(n,m,q\) 同阶则取 \(S=O(n^{\frac{2}{3}})\)(一般都取这个)。总复杂度 \(O(n^{\frac{5}{3}})\)。
例题
P1903 [国家集训队] 数颜色 / 维护队列
在加入修改时如果在当前区间内就更新答案和对应位置的值,否则仅更新值。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
struct stru {
int s[N], ans;
stru() { ans = 0; memset(s, 0, sizeof(s)); }
inline void add(int x) { ans += !s[x], s[x]++; }
inline void del(int x) { s[x]--, ans -= !s[x]; }
} S;
int a[N], b[N], ans[N];
struct qry {
int l, r, t, id;
bool operator <(qry x) const {
if (b[l] != b[x.l]) return l < x.l;
else if (b[r] != b[x.r]) return r < x.r;
else return t < x.t;
}
} Q[N];
struct upd { int pos, co; } U[N];
inline void upd(int l, int r, upd &y) {
if (l <= y.pos && y.pos <= r)
S.del(a[y.pos]), S.add(y.co);
swap(a[y.pos], y.co);
// 后面撤销一定是一个个换回来的,所以 swap 即可
// 注意这里的 y 要引用传递
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m;
int B = cbrt(1ll * n * n), ts = 0;
for (int i = 1; i <= n; i++)
cin >> a[i], b[i] = (i - 1) / B + 1;
for (int i = 1, tim = 0; i <= m; i++) {
char op; cin >> op;
// 将询问和修改分开处理,以查询为时间轴
if (op == 'Q') {
ts++, Q[ts].id = ts, Q[ts].t = tim;
cin >> Q[ts].l >> Q[ts].r;
}
else tim++, cin >> U[tim].pos >> U[tim].co;
}
sort(Q + 1, Q + 1 + ts);
int l = 0, r = 0, t = 0;
for (int i = 1; i <= ts; i++) {
while (r < Q[i].r) S.add(a[++r]);
while (l > Q[i].l) S.add(a[--l]);
while (r > Q[i].r) S.del(a[r--]);
while (l < Q[i].l) S.del(a[l++]);
while (t < Q[i].t) upd(l, r, U[++t]);
while (t > Q[i].t) upd(l, r, U[t--]);
ans[Q[i].id] = S.ans;
}
for (int i = 1; i <= ts; i++) cout << ans[i] << '\n';
return 0;
}
树上莫队
树上莫队可以求树上路径信息。
转化为序列问题
写出原树的括号序(在进出 \(x\) 子树时加入 \(x\)),那么查询路径问题便可转化为序列问题。
具体地,查询 \(a,b\) 路径信息(不妨设 \(dfn_a<dfn_b\))就可转化为查询括号序上的第一个 \(a\) 和括号序上的第一个 \(b\) 之间的答案。
和正常莫队有点不同:出现两遍是抵消,所以需要维护一个 \(vis\) 表示是否被统计,修改即异或 \(1\)。
注意若 \(a\) 不是 \(b\) 的祖先的话,这样 \(\operatorname{lca}(a,b)\) 和 \(a\) 会被漏算,统计答案时记得加上去。
真·树上莫队
定义 \(S(a,b)\) 为 \(a,b\) 路径上不包含 \(\operatorname{lca}(a,b)\) 的点集(不包含是为了避免细节)。操作一次 \(S(a,b)\) 表示将 \(S(a,b)\) 中的点\(vis\) 取反并更新答案(\(vis\) 定义同上,表示是否被统计)。
那么考虑 \(S(a,b)\) 到 \(S(c,d)\) 如何更新答案。其实就相当于分别操作一次 \(S(a,b)\) 和 \(S(c,d)\)。
结论:修改 \(S(a,c)\) 和 \(S(b,d)\) 的状态(\(vis\) 取反并更新答案)。
得到 \(S(x,y)\) 可以看做操作 \(S(rt,x)\) 和 \(S(rt,y)\) 一遍,反之亦然。
所以操作 \(S(a,b)\) 和 \(S(c,d)\) 一次相当于操作 \(S(rt,a),S(rt,b),S(rt,c),S(rt,d)\),将 \(S(rt,a)\) 与 \(S(rt,c)\)、\(S(rt,b)\) 与 \(S(rt,d)\) 合并起来就是 \(S(a,c)\) 和 \(S(b,d)\)。
这样一看,这 \(a,b\) 不就相当于序列莫队的 \(l,r\)?那能不能找到一种分块方法使得复杂度得以保障?
这题提供了一个方法,可以把子树上的点划分成若干块,使得每块的大小在 \(B\sim 3B\),且每个块在加入至多一个点后都是联通的。
遍历整棵树自叶子至根递归处理,不能合并了就加入当前点留到父亲。具体地,对于 \(x\) 把 \(x\) 的儿子合并一些(不带 \(x\),将 \(x\) 当成“加入的点”),剩下的留到父亲(由于加入了当前点一定联通)。
若根有留下来的就合并到最后一个,可以证明符合要求。
以此方法实现的树分块(非上一题):
vector <int> p[N];
stack <int> q;
void dfs(int k, int fa) {
int st = q.size(); // 前面的是别的子树的,不能放在当前子树
for (auto i : p[k])
if (i != fa) {
dfs(i, k);
if (q.size() - st >= B) {
ccnt++;
while (q.size() > st)
bid[q.top()] = ccnt, q.pop();
}
}
q.push(k);
if (!fa) { // 根节点得新建一个
ccnt++;
while (!q.empty())
bid[q.top()] = ccnt, q.pop();
}
}
// 最后其实不用合并,因为一个块影响不了复杂度
在最基本的树上莫队中是按左端点所在块编号为第一关键字、右端点 \(dfn\) 升序排序。注意在输入时得将 \(dfn\) 较小的一个当成 \(l\)。
时间复杂度:和序列莫队其实一样,都是 \(O(nS+\frac{n^2}{S})\)。
需要特别关注的是按 \(dfn\) 升序访问一遍所有点和按编号大小访问一遍所有块的复杂度都是 \(O(n)\) 的。(这就相当于几乎所有操作复杂度都与序列相同,决定了在带修莫队等中这样的做法也是可行的)。
证明:
第一个比较简单,因为每条边最多被访问两次。
第二个会稍微复杂一点。观察到如果把所有的块缩成一个点(会存在一些块不连通,但加入一个点就可以连通,不妨加进去,因为不影响复杂度),那么形成的应该还是一棵树。
与第一个不同,这里的编号并不是按照 \(dfn\) 序排列的(其实可以排个序,不过有点麻烦)。
但是,它仍有很好的性质:对于块 \(x,y(dfn_x<dfn_y)\) 满足 \(id_x<id_y\),则对于 \(fx=fa_x\) 和 \(fy=fa_y\) 满足 \(fx,fy\) 均不为 \(x,y\) 的公共祖先,那么 \(id_{fx}<id_{fy}\)。
什么意思?这样的访问顺序和按 \(dfn\) 访问时一样的,因为出子树时所有的点都会被访问到!
实际每个块的大小是 \(O(S)\) 的,所以总复杂度应该也是 \(O(\frac{n}{S}\times S)=O(n)\)。
由于树分块的大小不稳定,因此最优块长可能会略有偏差。
例题
SP10707 COT2 - Count on a tree II
板子,代码写的是括号序。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
const int M = 20;
int a[N], lsh[N], b[N], fi[N];
vector <int> p[N];
struct qry {
int l, r, id;
bool operator <(qry x) const {
if (b[l] != b[x.l]) return l < x.l;
else return r < x.r;
}
} Q[N];
int tr[N], len, dfn[N], ts, f[N][M];
void dfs(int k, int fa) {
tr[++len] = k, fi[k] = len;
f[dfn[k] = ++ts][0] = fa;
for (auto i : p[k])
if (i != fa) dfs(i, k);
tr[++len] = k;
}
inline int chmax(int x, int y) {
return dfn[x] < dfn[y] ? x : y;
}
inline int lca(int x, int y) {
if (x == y) return x; x = dfn[x], y = dfn[y];
if (x > y) swap(x, y); int k = __lg(y - x);
return chmax(f[x + 1][k], f[y - (1 << k) + 1][k]);
}
struct Stru {
int ans, cnt[N];
bool vis[N];
Stru() {
ans = 0;
memset(cnt, 0, sizeof(cnt));
memset(vis, 0, sizeof(vis));
}
inline void add(int x) { ans += !cnt[x], cnt[x]++; }
inline void del(int x) { cnt[x]--, ans -= !cnt[x]; }
inline void rev(int id) {
vis[id] ^= 1, vis[id] ? add(a[id]) : del(a[id]);
}
} S;
int ans[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i], lsh[i] = a[i];
sort(lsh + 1, lsh + 1 + n);
int ts = unique(lsh + 1, lsh + 1 + n) - lsh - 1;
for (int i = 1; i <= n; i++)
a[i] = lower_bound(lsh + 1, lsh + 1 + ts, a[i]) - lsh;
for (int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
p[u].push_back(v);
p[v].push_back(u);
}
dfs(1, 0); int B = sqrt(len);
for (int j = 1; j < M; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++)
f[i][j] = chmax(f[i][j - 1], f[i + (1 << j - 1)][j - 1]);
for (int i = 1; i <= len; i++)
b[i] = (i - 1) / B + 1;
for (int i = 1; i <= m; i++) {
int x, y; cin >> x >> y; Q[i].id = i;
Q[i].l = fi[x], Q[i].r = fi[y];
if (Q[i].l > Q[i].r) swap(Q[i].l, Q[i].r);
}
sort(Q + 1, Q + 1 + m);
for (int i = 1, l = 1, r = 0; i <= m; i++) {
while (r < Q[i].r) S.rev(tr[++r]);
while (l > Q[i].l) S.rev(tr[--l]);
while (r > Q[i].r) S.rev(tr[r--]);
while (l < Q[i].l) S.rev(tr[l++]);
int L = lca(tr[Q[i].l], tr[Q[i].r]);
if (L == tr[Q[i].l]) ans[Q[i].id] = S.ans;
else {
S.rev(tr[Q[i].l]), S.rev(L);
ans[Q[i].id] = S.ans;
S.rev(tr[Q[i].l]), S.rev(L);
}
}
for (int i = 1; i <= m; i++)
cout << ans[i] << '\n';
return 0;
}
P4074 [WC2013] 糖果公园
树上带修莫队,这里使用树分块的写法。
经测试,这题块长取 \(n^{0.63}\) 比较优,但取定长 \(1000\) 和 \(n^{\frac{2}{3}}\) 等等都是可以过的。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
const int M = 20;
int bid[N], dfn[N], f[M][N], B, ts, ccnt;
vector <int> p[N];
stack <int> q;
void dfs(int k, int fa) {
f[0][dfn[k] = ++ts] = fa;
int st = q.size();
for (auto i : p[k])
if (i != fa) {
dfs(i, k);
if (q.size() - st >= B) {
ccnt++;
while (q.size() > st)
bid[q.top()] = ccnt, q.pop();
}
}
q.push(k);
if (!fa) {
ccnt++;
while (!q.empty())
bid[q.top()] = ccnt, q.pop();
}
}
inline int chmax(int x, int y) {
return dfn[x] < dfn[y] ? x : y;
}
inline void init_lca(int n) {
for (int j = 1; j < M; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++)
f[j][i] = chmax(f[j - 1][i], f[j - 1][i + (1 << j - 1)]);
}
inline int lca(int x, int y) {
if (x == y) return x; x = dfn[x], y = dfn[y];
if (x > y) swap(x, y); int k = __lg(y - x);
return chmax(f[k][x + 1], f[k][y - (1 << k) + 1]);
}
int w[N], nw[N], ans[N], a[N];
struct updata { int pos, w; } U[N];
struct Stru {
int ans, cnt[N];
bool vis[N];
Stru() {
ans = 0;
memset(cnt, 0, sizeof(cnt));
memset(vis, 0, sizeof(vis));
}
inline void add(int x) {
ans += w[x] * nw[++cnt[x]];
}
inline void del(int x) {
ans -= w[x] * nw[cnt[x]--];
}
inline void rev(int id) {
vis[id] ? del(a[id]) : add(a[id]);
vis[id] ^= 1;
}
inline void upd(updata &x) {
if (vis[x.pos])
del(a[x.pos]), add(x.w);
swap(a[x.pos], x.w);
}
}S;
struct qry {
int l, r, id, t;
bool operator <(qry x) const {
if (bid[l] != bid[x.l]) return bid[l] < bid[x.l];
else if (bid[r] != bid[x.r]) return bid[r] < bid[x.r];
else return t < x.t;
}
} Q[N];
inline void upd(int x, int y) {
int l = lca(x, y);
while (x != l) S.rev(x), x = f[0][dfn[x]];
while (y != l) S.rev(y), y = f[0][dfn[y]];
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m, q; cin >> n >> m >> q;
for (int i = 1; i <= m; i++) cin >> w[i];
for (int i = 1; i <= n; i++) cin >> nw[i];
for (int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
p[u].push_back(v);
p[v].push_back(u);
}
for (int i = 1; i <= n; i++) cin >> a[i];
B = pow(n, 0.63); dfs(1, 0), init_lca(n);
int ts = 0, t = 0;
for (int i = 1; i <= q; i++) {
int op; cin >> op;
if (!op) t++, cin >> U[t].pos >> U[t].w;
else {
ts++, Q[ts].id = ts, Q[ts].t = t;
cin >> Q[ts].l >> Q[ts].r;
if (dfn[Q[ts].l] > dfn[Q[ts].r]) swap(Q[ts].l, Q[ts].r);
}
}
sort(Q + 1, Q + 1 + ts); int l, r;
upd(l = Q[1].l, r = Q[1].r);
for (int i = 1, t = 0; i <= ts; i++) {
if (i != 1) {
upd(Q[i].l, Q[i - 1].l);
upd(Q[i].r, Q[i - 1].r);
}
while (t < Q[i].t) S.upd(U[++t]);
while (t > Q[i].t) S.upd(U[t--]);
int l = lca(Q[i].l, Q[i].r);
S.rev(l), ans[Q[i].id] = S.ans, S.rev(l);
}
for (int i = 1; i <= ts; i++)
cout << ans[i] << '\n';
return 0;
}
莫队二次离线
若区间的移动和拓展不能做到 \(O(1)\),那么可以尝试莫队二次离线。
设 \(i\) 对区间 \([l,r]\) 的贡献为 \(f(i,[l,r])\)。以 \(r\) 的拓展为例,若 \(r\to r'\),则需要对于 \(\forall i\in[r+1,r']\) 求出 \(f(i,[l,i-1])\)。
如果信息满足可减性(前提),则有:
第一部分可以直接预处理出来。而第二部分可以离线下来扫描线做,由莫队的复杂度分析可以知道第二部分的 \(f\) 的数量是 \(O(nS)\) 级别的。
一个空间的优化:对于同一个询问,贡献区间都为 \([1,l-1]\),只存 \(r+1,r'\) 可以做到线性空间。
同理,左端点左移就是:\(f(i,[i+1,r])=f(i,[i,r])-f(i,[1,i])\)。如果是缩小范围就取反一下。
注意对于 \(f(i,[i+1,r])\) 和 \(f(i,[l,i-1])\) 的具体算的东西可能不同,典型的例子就是区间逆序对。
设原来莫队左右端点拓展一个是 \(O(m)\),则时间复杂度由 \(O(nm\sqrt n)\) 变为了 \(O(n\sqrt n+nm)\)。
例题
P4887 【模板】莫队二次离线(第十四分块(前体))
\(a\oplus b=c\Leftrightarrow a\oplus c=b\),于是开个桶记录一下。
这题由于 \(i<j\),所以可以偷个小懒:\(f(i,[1,i])=f(i,[1,i-1])\)。同时在最后统计答案的时候注意若 \(k=0\) 则要把 \(i=j\) 的情况去掉。
最坏复杂度为 \(O(\tbinom{14}{7}n+n\sqrt n)\)。
#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;
const int N = 1e5 + 5;
const int M = 1 << 14;
int a[N], cnt[M], f[N], b[N], ans[N];
vector <int> buc;
struct query { int l, r, id; } Q[N];
bool operator <(query x, query y) {
if (b[x.l] ^ b[y.l]) return x.l < y.l;
else return x.r < y.r;
}
struct init { int l, r, id, sgn; };
vector <init> p[N];
signed main() {
int n, m, k; cin >> n >> m >> k; int B = sqrt(n);
for (int i = 0; i < M; i++)
if (__builtin_popcount(i) == k) buc.pb(i);
for (int i = 1; i <= n; i++) {
cin >> a[i];
for (auto j : buc)
f[i] += cnt[a[i] ^ j];
cnt[a[i]]++;
}
for (int i = 1; i <= n; i++)
b[i] = (i - 1) / B + 1;
for (int i = 1; i <= m; i++)
cin >> Q[i].l >> Q[i].r, Q[i].id = i;
sort(Q + 1, Q + 1 + m);
for (int i = 1, l = 1, r = 0; i <= m; i++) {
// 先扩大后缩小好习惯
if (r < Q[i].r) p[l - 1].pb({r + 1, Q[i].r, Q[i].id, -1});
while (r < Q[i].r) ans[Q[i].id] += f[++r];
if (l > Q[i].l) p[r].pb({Q[i].l, l - 1, Q[i].id, 1});
while (l > Q[i].l) ans[Q[i].id] -= f[--l];
if (r > Q[i].r) p[l - 1].pb({Q[i].r + 1, r, Q[i].id, 1});
while (r > Q[i].r) ans[Q[i].id] -= f[r--];
if (l < Q[i].l) p[r].pb({l, Q[i].l - 1, Q[i].id, -1});
while (l < Q[i].l) ans[Q[i].id] += f[l++];
}
memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; i++) {
for (auto j : buc) cnt[a[i] ^ j]++;
for (auto j : p[i])
for (int p = j.l; p <= j.r; p++)
ans[j.id] += j.sgn * (cnt[a[p]] - (p <= i && !k));
// p <= i 就会算重,而不仅仅是 p=i
}
for (int i = 1; i <= m; i++)
ans[Q[i].id] += ans[Q[i - 1].id];
// 之前 ans 存的是答案的差分,这里要做一遍前缀和
for (int i = 1; i <= m; i++)
cout << ans[i] << '\n';
return 0;
}

浙公网安备 33010602011771号