【学习笔记】莫队

莫队是一种离线算法,用于求区间信息。

普通莫队

假设已知 \([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\),那么答案则为:

\[\frac{\sum_{i=1}^n\dbinom{cnt_i}{2}}{\dbinom{len}{2}}=\frac{\sum_{i=1}^ncnt_i^2-cnt_i}{len(len-1)}=\frac{(\sum_{i=1}^ncnt_i^2)-len}{len(len-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 的烦恼

重点在于 adddel 的实现。

具体来说,记 \(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;
}

练习

带修莫队

上述莫队都是无法支持修改的,如果题目有单点修改怎么办?

不妨考虑一个暴力的方法:把原来的 \(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\)?那能不能找到一种分块方法使得复杂度得以保障?


前置题目:P2325 [SCOI2005] 王室联邦

这题提供了一个方法,可以把子树上的点划分成若干块,使得每块的大小在 \(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(i,[l,i-1])=f(i,[1,i-1])-f(i,[1,l-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;
}
posted @ 2025-05-31 16:03  happy_zero  阅读(40)  评论(0)    收藏  举报