回滚莫队学习笔记

DimStar

它解决什么问题

普通莫队靠一双 l, r 指针来回横跳,要求加入和删除都。但有些信息天然不对称:比如维护按 dfn 序相邻点对的距离和,从链表删除一个点只动两条边,是 ;加入一个点却要先找到它的前驱后继,开销可达 ,或用平衡树,但此时两种操作都是 。反过来”加入容易、删除困难”的情况也有。

回滚莫队的思路是:结合分块思想和回滚操作。让它每块只发生一次昂贵的初始化,并把开销大的操作通过“撤销”实现,单次询问只做便宜的操作。

根据是哪种操作更为容易,它分两种形态:

  • 只加回滚(易加难删):左端点临时加入,做完回滚;
  • 只删回滚(易删难加):左端点临时删除,做完回滚。

两种形态的骨架

设块大小为 。

只加回滚:块内按 升序。

1
2
3
4
5
6
7
8
9
10
11
12
13
for (每个块 bid) {
清空结构;
int curR = blockR(bid);
for (每个询问 [l, r] 属于该块) {
if (r <= blockR(bid)) { 暴力算; continue; }
while (curR < r) add(a[++curR]); // 永久加入右半部分
int snap = snapshot();
int curL = blockR(bid) + 1;
while (curL > l) add(a[--curL]); // 临时加入左半部分
记录答案;
rollback(snap);
}
}

只删回滚:块内按 降序。

1
2
3
4
5
6
7
8
9
10
11
12
for (每个块 bid) {
把 [blockL(bid), m] 整体初始化成一个完整状态;
int curR = m;
for (每个询问 [l, r] 属于该块) {
while (curR > r) 永久删除 a[curR--]; // 永久删除右半部分
int snap = snapshot();
int curL = blockL(bid);
while (curL < l) 临时删除 a[curL++]; // 临时删除左半部分
记录答案;
rollback(snap);
}
}

右指针在每块内单调扫一遍 ,左指针每次最多扫 步,回滚次数同左指针。总时间复杂度 ,平衡后 。

例题:P9340 旅行

给一棵 个点的树和一个长度为 的序列 ,每次询问 ,问 中所有点去重后形成的点集在树上形成的最小连通块点数。。

根据结论,将点集按 dfn 排为 后,边数等于 ,点数则等于边数加一。

这道题是典型的易删难加,删除在初始化后容易,但插入困难,开头已经提过了,使用只删回滚莫队即可。

流程:

  1. 左端点分块,块内按右端点降序排;
  2. 进新块时,把 按 dfn 连成环状链表,算出环上相邻距离和 now;
  3. 右指针从 左移到 ,永久删除,即 DEL;
  4. 记录 now,左指针从 blockL 右移到 ,临时删除,即 del,同时把被动的指针压栈;
  5. 输出 now/2 + 1,now 恢复,弹栈回滚指针和 cnt。

代码:

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#include <bits/stdc++.h>
#define nmf(i, s, e) for (int i = s; i <= e; i++)
#define ref(i, s, e) for (int i = s; i >= e; i--)
using namespace std;
typedef long long LL;
typedef unsigned long long uLL;
int n, m, q, B;
vector<int> grh[100005];
int c[100005];
int dfn[100005], tim;
int dep[100005];
int lg[100005];
int st[19][100005];
void dfs(int u, int fa)
{
dfn[u] = ++tim;
dep[tim] = dep[dfn[fa]] + 1;
st[0][tim] = dfn[fa];
for (auto v : grh[u])
{
if (v == fa)
continue;
dfs(v, u);
}
}
void initLCA()
{
lg[0] = -1;
nmf(i, 1, tim) lg[i] = lg[i >> 1] + 1;
for (int k = 1; (1 << k) <= tim; k++)
{
nmf(i, 1, tim - (1 << k) + 1)
{
int a = st[k - 1][i];
int b = st[k - 1][i + (1 << (k - 1))];
st[k][i] = (dep[a] <= dep[b] ? a : b);
}
}
}
inline int LCA(int l, int r)
{
if (l == r)
return l;
if (l > r)
swap(l, r);
++l;
int k = lg[r - l + 1];
int a = st[k][l];
int b = st[k][r - (1 << k) + 1];
return (dep[a] <= dep[b] ? a : b);
}
inline int dist(int u, int v)
{
if (!u || !v)
return 0;
return dep[u] + dep[v] - 2 * dep[LCA(u, v)];
}
struct Query
{
int l, r, id;
int bel;
};
bool cmp(const Query &x, const Query &y)
{
return (x.bel != y.bel) ? (x.l < y.l) : (x.r > y.r);
}
Query qries[100005];
int ans[100005];
int cnt[100005];
int pre[100005], nxt[100005];
int now;
struct list_node
{
int pre, nxt, id;
};
stack<list_node, vector<list_node>> stk;
inline void DEL(int x)
{
x = dfn[c[x]];
cnt[x]--;
if (!cnt[x])
{
now -= dist(pre[x], x);
now -= dist(x, nxt[x]);
now += dist(pre[x], nxt[x]);
pre[nxt[x]] = pre[x];
nxt[pre[x]] = nxt[x];
}
}
inline void del(int x)
{
x = dfn[c[x]];
stk.push({pre[x], nxt[x], x});
cnt[x]--;
if (!cnt[x])
{
now -= dist(pre[x], x);
now -= dist(x, nxt[x]);
now += dist(pre[x], nxt[x]);
pre[nxt[x]] = pre[x];
nxt[pre[x]] = nxt[x];
}
}
void solve()
{
cin >> n >> m >> q;
nmf(i, 1, n - 1)
{
int u, v;
cin >> u >> v;
grh[u].push_back(v);
grh[v].push_back(u);
}
dfs(1, 0);
initLCA();
nmf(i, 1, m) cin >> c[i];
B = max(1, (int)sqrt((double)m));
nmf(i, 1, q)
{
cin >> qries[i].l >> qries[i].r;
qries[i].id = i;
qries[i].bel = (qries[i].l + B - 1) / B;
}
sort(qries + 1, qries + 1 + q, cmp);
int l = 1, r = 0;
nmf(idx, 1, q)
{
if (qries[idx].bel > qries[idx - 1].bel)
{
memset(cnt, 0, sizeof(cnt));
now = 0;
nmf(i, (qries[idx].bel - 1) * B + 1, m) cnt[dfn[c[i]]]++;
int las = -1;
nmf(i, 1, n) if (cnt[i]) pre[i] = las, las = i;
nmf(i, 1, n) if (cnt[i] && !(~pre[i])) pre[i] = las;
las = -1;
ref(i, n, 1) if (cnt[i]) nxt[i] = las, las = i;
ref(i, n, 1) if (cnt[i] && !(~nxt[i])) nxt[i] = las;
nmf(i, 1, n) if (cnt[i]) now += dist(pre[i], i);
r = m;
l = (qries[idx].bel - 1) * B + 1;
}
auto [ql, qr, qid, _] = qries[idx];
while (r > qr)
{
DEL(r--);
}
int tmp = now;
while (l < ql)
{
del(l++);
}
ans[qid] = (now >> 1) + 1;
now = tmp;
while (!stk.empty())
{
auto tp = stk.top();
stk.pop();
if (!cnt[tp.id])
nxt[tp.pre] = tp.id, pre[tp.nxt] = tp.id;
cnt[tp.id]++;
}
l = (qries[idx].bel - 1) * B + 1;
}
nmf(i, 1, q) cout << ans[i] << '\n';
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int T = 1;
while (T--)
solve();
return 0;
}

小声说:这玩意常数怎么这么大,常数是 cdo 写的树链剖分加树状数组的双老哥做法的接近十倍。

  • 标题: 回滚莫队学习笔记
  • 作者: DimStar
  • 创建于 : 2026-10-27 15:09:58
  • 更新于 : 2026-11-06 18:19:26
  • 链接: https://dimstar-zhang.github.io/2026/10/27/回滚莫队学习笔记/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。