莫队变种
咦,莫队怎么变异了?
回滚莫队
莫队怎么会滚了???
对于传统的莫队来说,一些麻烦的操作它并不能很好的处理,回滚莫队专治这种疑难杂症。
回滚莫队主要处理删除难、增加简单或者增加难,删除简单。
而他的核心就是只加不减或者只减不加。
核心步骤
1. 对查询进行排序
对所有询问区间 \([l, r]\) 进行排序:
-
第一关键字:左端点 \(l\) 所属的块编号,从小到大升序。
-
第二关键字:右端点 \(r\) 的位置,从小到大升序。
\(\color{yellow}Warning\):回滚莫队不能像普通莫队那样进行奇偶性排序,因为我们要保证 \(R\) 在同一个块内严格单调,奇偶排序会破坏单调性。
2. 按块处理查询
依次处理每一个查询。假设当前查询的左端点所在的块编号为 \(B\)。
- 询问的左右端点在同一个块内
如果 \(l\) 和 \(r\) 属于同一个块:
直接暴力扫描区间 \([l, r]\) 得到答案,记录答案后,立刻将这部分暴力加入的暴力删除。
- 询问的左右端点不在同一个块内
如果左端点所在的块 \(X\) 与上一个查询不同,说明进入了一个新块:
初始化将莫队区间的左端点初始化为 \(X\) 的右边界加 1,即 \(l = R_X + 1\)。将右端点初始化为 \(B\) 的右边界,即 \(r = R_X\)。此时区间 \([l, r]\) 为空集,清空全局维护的答案。
接下来,处理这个块 \(B\) 内的所有跨块查询:
- 单调扩展右端点:因为同一个块内,查询的右端点 \(r\) 是升序的。如果当前的 \(r > R\),我们不断将 \(R\) 向右扩展(执行增加操作),直到 \(R = r\)。
- 记录当前状态:此时右半部分 \([R_X+1, r ]\) 的信息已经统计完毕。记录下当前的答案(比如当前的
max),作为基础答案。 - 临时扩展左端点:不断将莫队左端点 \(L\) 向左扩展,直到 \(L = l\)(执行增加操作)。此时区间完整的 \([l, r]\) 信息统计完毕。
- 回答询问:记录当前查询的最终答案。
- 回滚左端点:将左指针从 \(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;
}
总结一哈
- 回滚莫队主要处理一种好处理,另一种不好处理的情况。
- 回滚莫队是一种通过不断回溯来解决不同的区间
\(\color{white}{滥用标题行} \color{black}\Huge 未完待续……?\)
树上莫队
首先,需要知道欧拉序
欧拉序就是你在 dfs 一棵树的时候,你第一次遍历到这个点与最后一次遍历这个点,有点像线段树分治。
举个例子。
\(1\) 为根节点, \(2,3\) 为 \(1\) 的儿子
- 遍历节点 \(1\)
- 遍历节点 \(2\)
- 退出节点 \(2\)
- 遍历节点 \(3\)
- 退出节点 \(3\)
- 退出节点 \(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;
}

浙公网安备 33010602011771号