莫队变种

咦,莫队怎么变异了?

回滚莫队

莫队怎么会滚了???

对于传统的莫队来说,一些麻烦的操作它并不能很好的处理,回滚莫队专治这种疑难杂症。

回滚莫队主要处理删除难、增加简单或者增加难,删除简单。

而他的核心就是只加不减或者只减不加

核心步骤

1. 对查询进行排序

对所有询问区间 \([l, r]\) 进行排序:

  • 第一关键字:左端点 \(l\) 所属的块编号,从小到大升序。

  • 第二关键字:右端点 \(r\) 的位置,从小到大升序。

\(\color{yellow}Warning\):回滚莫队不能像普通莫队那样进行奇偶性排序,因为我们要保证 \(R\) 在同一个块内严格单调,奇偶排序会破坏单调性。

2. 按块处理查询

依次处理每一个查询。假设当前查询的左端点所在的块编号为 \(B\)

  1. 询问的左右端点在同一个块内

如果 \(l\)\(r\) 属于同一个块:

直接暴力扫描区间 \([l, r]\) 得到答案,记录答案后,立刻将这部分暴力加入的暴力删除。

  1. 询问的左右端点不在同一个块内

如果左端点所在的块 \(X\) 与上一个查询不同,说明进入了一个新块

初始化将莫队区间的左端点初始化为 \(X\) 的右边界加 1,即 \(l = R_X + 1\)。将右端点初始化为 \(B\) 的右边界,即 \(r = R_X\)。此时区间 \([l, r]\) 为空集,清空全局维护的答案。

接下来,处理这个块 \(B\) 内的所有跨块查询:

  1. 单调扩展右端点:因为同一个块内,查询的右端点 \(r\) 是升序的。如果当前的 \(r > R\),我们不断将 \(R\) 向右扩展(执行增加操作),直到 \(R = r\)
  2. 记录当前状态:此时右半部分 \([R_X+1, r ]\) 的信息已经统计完毕。记录下当前的答案(比如当前的 max),作为基础答案。
  3. 临时扩展左端点:不断将莫队左端点 \(L\) 向左扩展,直到 \(L = l\)(执行增加操作)。此时区间完整的 \([l, r]\) 信息统计完毕。
  4. 回答询问:记录当前查询的最终答案。
  5. 回滚左端点:将左指针从 \(l\) 重新拉回到 \(R_X + 1\)

就例如维护区间最大值。

例题:P14420 [JOISC 2014] 历史的研究 / Historical Research

为什么模版是紫?又可以水一道紫题啦!

在处理的时候就按照上面的操作处理,然后就直接记录就行了。

食用代码会更好理解

戳我喵~
int a[N], b[N], val[N];
int cnt[N], tmp[N];
int pos[N], R[N];

struct Query {
	int l, r, id;
}q[N];

int ans[N];

signed main() {
	int n = re, m = re;
	for (int i = 1; i <= n; i++) a[i] = b[i] = re;
	sort(b + 1, b + 1 + n);
	int KKK = unique(b + 1, b + 1 + n) - b - 1;
	for (int i = 1; i <= n; i++) {
		int pos = lower_bound(b + 1, b + 1 + KKK, a[i]) - b; //离散化
		val[pos] = a[i], a[i] = pos;
	}
	int block = max(1.0, n / sqrt(m)), sum = (n + block - 1) / block;
	for (int i = 1; i <= n; i++) pos[i] = (i - 1) / block + 1;
	for (int i = 1; i <= sum; i++) R[i] = min(n, i * block);
	for (int i = 1; i <= m; i++) q[i] = {re, re, i};
	sort(q + 1, q + 1 + m, [](const Query &x, const Query &y) { //排序
		return pos[x.l] == pos[y.l] ? x.r < y.r : pos[x.l] < pos[y.l];
	});
	int lst = 0, l = 1, r = 0, maxn = 0;
	for (int i = 1; i <= m; i++) {
		if (pos[q[i].l] == pos[q[i].r]) { //暴力!
			int Ans = 0;
			for (int j = q[i].l; j <= q[i].r; j++) tmp[a[j]]++, Ans = max(Ans, tmp[a[j]] * val[a[j]]);
			ans[q[i].id] = Ans;
			for (int j = q[i].l; j <= q[i].r; j++) tmp[a[j]]--;
			continue;
		}
		if (pos[q[i].l] != lst) { //不在同一个块
			for (int j = R[lst] + 1; j <= r; j++) cnt[a[j]]--;
			lst = pos[q[i].l], maxn = 0, r = R[lst], l = R[lst] + 1;
		}
		while (r < q[i].r) { //移动指针
			r++, cnt[a[r]]++, maxn = max(maxn, cnt[a[r]] * val[a[r]]);
		}
		int t = maxn, tl = l; //需要使用临时指针
		while (tl > q[i].l) { //移动指针
			tl--, cnt[a[tl]]++, t = max(t, cnt[a[tl]] * val[a[tl]]);
		}
		ans[q[i].id] = t;
		while (tl < l) { //回滚
			cnt[a[tl]]--;
			tl++;
		}
	}
	for (int i = 1; i <= m; i++) wr(ans[i]), endl;
}

总结一哈

  1. 回滚莫队主要处理一种好处理,另一种不好处理的情况。
  2. 回滚莫队是一种通过不断回溯来解决不同的区间

\(\color{white}{滥用标题行} \color{black}\Huge 未完待续……?\)

树上莫队

首先,需要知道欧拉序

欧拉序就是你在 dfs 一棵树的时候,你第一次遍历到这个点与最后一次遍历这个点,有点像线段树分治。

举个例子。

\(1\) 为根节点, \(2,3\)\(1\) 的儿子

  1. 遍历节点 \(1\)
  2. 遍历节点 \(2\)
  3. 退出节点 \(2\)
  4. 遍历节点 \(3\)
  5. 退出节点 \(3\)
  6. 退出节点 \(1\)

那么欧拉序就是 \(1,2,2,3,3,1\)

非常的简单易懂。

我们需要记录两个关键数组:

  • \(st[u]\):节点 \(u\) 第一次出现在欧拉序中的下标。
  • \(ed[u]\):节点 \(u\) 第二次出现在欧拉序中的下标。

树上路径转成序列区间

现在我们有了一个长度为 \(2n\) 的一维数组。假设我们要查询节点 \(u\) 到节点 \(v\) 的路径信息。为了方便,我们始终保证 \(st[u] \le st[v]\)

这里分两种情况:

  • 情况 \(1\): \(u\)\(v\) 的祖先(即 \(LCA(u, v) = u\)),对应区间\([st[u], st[v]]\)
    解释:在 dfs 时,我们从 \(u\) 一路走到 \(v\)。在这个区间内,路径上的所有节点都只被访问了一次(只进入还未离开)。而那些属于 \(u\) 的分支但不在 \(v\) 路径上的节点,会被“进入一次,离开一次”(出现两次)。

  • 情况 \(2\)\(u\)\(v\) 分属不同分支(即 \(LCA(u, v) \neq u\)\(\neq v\)),对应区间\([ed[u], st[v]]\)
    解释:在 DFS 时,我们从 \(u\) 的底部离开 \(u\)(记录 \(ed[u]\)),然后向上回溯,穿过一些中间节点,再进入 \(v\) 的分支直到 \(v\)(记录 \(st[v]\))。在这个区间内,路径上的节点同样只出现了一次,不在路径上的节点出现了两次
    注意:在这个区间 \([ed[u], st[v]]\) 中,它们的最近公共祖先并没有出现!因为我们在离开 \(u\) 时还没到达离开 LCA 的时刻,而进入 \(v\) 时早就进入过 LCA 了。因此,后续莫队查询完这个区间后,需要手动把 LCA 的贡献加上,记录完答案后再手动把 LCA 的贡献减去。

莫队指针移动

在确定了查询区间 \([l, r]\) 后,剩下的工作就和普通莫队一样了。

在欧拉序区间内,真正在路径上的节点只出现了 1 次,而不在路径上的节点出现了 2 次。

因此,当莫队的指针在欧拉序数组上移动,经过某个节点 \(x\) 时,我们不需要区分“向左移还是向右移”,也不需要区分“增加还是删除”,我们只需要记录节点 \(x\) 在当前区间内的出现次数:

使用一个 \(vis_x\) 数组记录节点 \(x\) 是否在当前维护的集合中。当指针经过欧拉序上的某个位置 \(i\) 时,该位置代表的节点是 \(x= order[i]\)

我们直接对 \(vis_x\) 进行取反\(\oplus 1\)

当如果 \(vis_x\) 变成了 \(1\):说明该节点出现了一次,将其加入贡献。
当如果 \(vis_x\) 变成了 \(0\):说明该节点出现了两次,将其删去贡献。

那么对于代码也简单了,直接附上例题与代码。

Count on a tree II

戳我喵~
int a[N], b[N];
vector<int> g[N];
int ord[N << 1], st[N], ed[N], dfn_clock;
int depth[N], fa[N][19];
int pos[N << 1], cnt[N], vis[N], ans[N], Ans;

struct Query {
	int l, r, id, lca;
	bool operator <(const Query &x) const {
		return pos[l] == pos[x.l] ? pos[l] & 1 ? r < x.r : r > x.r : pos[l] < pos[x.l];
	}
}q[N];

void dfs(int u, int Fa) {
	ord[++dfn_clock] = u, st[u] = dfn_clock, depth[u] = depth[Fa] + 1, fa[u][0] = Fa;
	for (int i = 1; i < 17; i++) fa[u][i] = fa[fa[u][i - 1]][i - 1];
	for (auto v : g[u]) if (v != Fa) dfs(v, u);
	ord[++dfn_clock] = u, ed[u] = dfn_clock;
}

int LCA(int u, int v) {
	if (depth[u] < depth[v]) swap(u, v);
	int X = depth[u] - depth[v];
	for (int i = 0; i < 17; i++) if ((X >> i) & 1) u = fa[u][i];
	if (u == v) return u;
	for (int i = 16; i >= 0; i--) if (fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
	return fa[u][0];
}

void solve(int x) {
	if (vis[x]) {
		cnt[a[x]]--;
		if (!cnt[a[x]]) Ans--;
	}
	else {
		if (!cnt[a[x]]) Ans++;
		cnt[a[x]]++;
	}
	vis[x] ^= 1;
}

signed main() {
	int n = re, m = re;
	for (int i = 1; i <= n; i++) a[i] = b[i] = re;
	sort(b + 1, b + 1 + n);
	int KKK = unique(b + 1, b + 1 + n) - b - 1;
	for (int i = 1; i <= n; i++) a[i] = lower_bound(b + 1, b + 1 + KKK, a[i]) - b;
	for (int i = 1; i < n; i++) {
		int u = re, v = re;
		g[u].push_back(v), g[v].push_back(u);
	}
	dfs(1, 0);
	int block = max(1.0, (2.0 * n) / sqrt(m));
	for (int i = 1; i <= 2 * n; i++) pos[i] = (i - 1) / block + 1;
	for (int i = 1; i <= m; i++) {
		int u = re, v = re;
		if (st[u] > st[v]) swap(u, v);
		int lca = LCA(u, v);
		if (lca == u) q[i] = {st[u], st[v], i, 0};
		else q[i] = {ed[u], st[v], i, lca};
	}
	sort(q + 1, q + 1 + m);
	int l = 1, r = 0;
	for (int i = 1; i <= m; i++) {
		while (l > q[i].l) solve(ord[--l]);
		while (r < q[i].r) solve(ord[++r]);
		while (l < q[i].l) solve(ord[l++]);
		while (r > q[i].r) solve(ord[r--]);
		if (q[i].lca) solve(q[i].lca);
		ans[q[i].id] = Ans;
		if (q[i].lca) solve(q[i].lca);
	}
	for (int i = 1; i <= m; i++) wr(ans[i]), endl;
}
posted @ 2026-07-23 19:39  OiLight  阅读(4)  评论(0)    收藏  举报