对于压行版,如果没有 `r == x.r` 的特判,当 l 属于同一奇数块且 r 相等时,会出现上面小细节中的问题(自己手动模拟一下),对于压行版,如果写成小于(大于)等于,则也会出现同样的问题。
-## 带修改
-
-请确保您已经会普通莫队算法了。如果您还不会,请先阅读上面的“普通莫队算法”。
-
-### 特点
-
-普通莫队是不能带修改的。
-
-我们可以强行让它可以修改,就像 DP 一样,可以强行加上一维 **时间维** , 表示这次操作的时间。
-
-时间维表示经历的修改次数。
-
-即把询问 $[l,r]$ 变成 $[l,r,time]$ 。
-
-那么我们的坐标也可以在时间维上移动,即 $[l,r,time]$ 多了一维可以移动的方向,可以变成:
-
-- $[l-1,r,time]$
-- $[l+1,r,time]$
-- $[l,r-1,time]$
-- $[l,r+1,time]$
-- $[l,r,time-1]$
-- $[l,r,time+1]$
-
-这样的转移也是 $O(1)$ 的,但是我们排序又多了一个关键字,再搞搞就行了。
-
-可以用和普通莫队类似的方法排序转移,做到 $O(n^{\frac{5}{3}})$ 。
-
-这一次我们排序的方式是以 $n^{\frac{2}{3}}$ 为一块,分成了 $n^{\frac{1}{3}}$ 块,第一关键字是左端点所在块,第二关键字是右端点所在块,第三关键字是时间。
-
-还是来证明一下时间复杂度(默认块大小为 $\sqrt{n}$ ):
-
-- 左右端点所在块不变,时间在排序后单调向右移,这样的复杂度是 $O(n)$ ;
-- 若左右端点所在块改变,时间一次最多会移动 n 个格子,时间复杂度 $O(n)$ ;
-- 左端点所在块一共有 $n^{\frac{1}{3}}$ 中,右端点也是 $n^{\frac{1}{3}}$ 种,一共 ${n^{\frac{1}{3}}}\times{n^{\frac{1}{3}}}=n^{\frac{2}{3}}$ 种,每种乘上移动的复杂度 $O(n)$ ,总复杂度 $O(n^{\frac{5}{3}})$ 。
-
-### 例题
-
-???+note "例题[「国家集训队」数颜色 / 维护队列](https://www.luogu.org/problem/P1903)"
-
- 题目大意:给你一个序列,M 个操作,有两种操作:
-
- 1. 修改序列上某一位的数字
- 2. 询问区间 $[l,r]$ 中数字的种类数(多个相同的数字只算一个)
-
-我们不难发现,如果不带操作 1(修改)的话,我们就能轻松用普通莫队解决。
-
-但是题目还带单点修改,所以用 **带修改的莫队** 。
-
-先考虑普通莫队的做法:
-
-- 每次扩大区间时,每加入一个数字,则统计它已经出现的次数,如果加入前这种数字出现次数为 $0$ ,则说明这是一种新的数字,答案 $+1$ 。然后这种数字的出现次数 $+1$ 。
-- 每次减小区间时,每删除一个数字,则统计它删除后的出现次数,如果删除后这种数字出现次数为 $0$ ,则说明这种数字已经从当前的区间内删光了,也就是当前区间减少了一种颜色,答案 $-1$ 。然后这种数字的出现次数 $-1$ 。
-
-现在再来考虑修改:
-
-- 单点修改,把某一位的数字修改掉。假如我们是从一个经历修改次数为 $i$ 的询问转移到一个经历修改次数为 $j$ 的询问上,且 $i<j$ 的话,我们就需要把第 $i+1$ 个到第 $j$ 个修改强行加上。
-- 假如 $j<i$ 的话,则需要把第 $i$ 个到第 $j+1$ 个修改强行还原。
-
-怎么强行加上一个修改呢?假设一个修改是修改第 $pos$ 个位置上的颜色,原本 $pos$ 上的颜色为 $a$ ,修改后颜色为 $b$ ,还假设当前莫队的区间扩展到了 $[l,r]$ 。
-
-- 加上这个修改:我们首先判断 $pos$ 是否在区间 $[l,r]$ 内。如果是的话,我们等于是从区间中删掉颜色 $a$ ,加上颜色 $b$ ,并且当前颜色序列的第 $pos$ 项的颜色改成 $b$ 。如果不在区间 $[l,r]$ 内的话,我们就直接修改当前颜色序列的第 $pos$ 项为 $b$ 。
-- 还原这个修改:等于加上一个修改第 $pos$ 项、把颜色 $b$ 改成颜色 $a$ 的修改。
-
-因此这道题就这样用带修改莫队轻松解决啦!
-
-??? 参考代码
- ```cpp
- #include <bits/stdc++.h>
- #define SZ (10005)
- using namespace std;
- template <typename _Tp>
- inline void IN(_Tp& dig) {
- char c;
- dig = 0;
- while (c = getchar(), !isdigit(c))
- ;
- while (isdigit(c)) dig = dig * 10 + c - '0', c = getchar();
- }
- int n, m, sqn, c[SZ], ct[SZ], c1, c2, mem[SZ][3], ans, tot[1000005], nal[SZ];
- struct query {
- int l, r, i, c;
- bool operator<(const query another) const {
- if (l / sqn == another.l / sqn) {
- if (r / sqn == another.r / sqn) return i < another.i;
- return r < another.r;
- }
- return l < another.l;
- }
- } Q[SZ];
- void add(int a) {
- if (!tot[a]) ans++;
- tot[a]++;
- }
- void del(int a) {
- tot[a]--;
- if (!tot[a]) ans--;
- }
- char opt[10];
- int main() {
- IN(n), IN(m), sqn = pow(n, (double)2 / (double)3);
- for (int i = 1; i <= n; i++) IN(c[i]), ct[i] = c[i];
- for (int i = 1, a, b; i <= m; i++)
- if (scanf("%s", opt), IN(a), IN(b), opt[0] == 'Q')
- Q[c1].l = a, Q[c1].r = b, Q[c1].i = c1, Q[c1].c = c2, c1++;
- else
- mem[c2][0] = a, mem[c2][1] = ct[a], mem[c2][2] = ct[a] = b, c2++;
- sort(Q, Q + c1), add(c[1]);
- int l = 1, r = 1, lst = 0;
- for (int i = 0; i < c1; i++) {
- for (; lst < Q[i].c; lst++) {
- if (l <= mem[lst][0] && mem[lst][0] <= r)
- del(mem[lst][1]), add(mem[lst][2]);
- c[mem[lst][0]] = mem[lst][2];
- }
- for (; lst > Q[i].c; lst--) {
- if (l <= mem[lst - 1][0] && mem[lst - 1][0] <= r)
- del(mem[lst - 1][2]), add(mem[lst - 1][1]);
- c[mem[lst - 1][0]] = mem[lst - 1][1];
- }
- for (++r; r <= Q[i].r; r++) add(c[r]);
- for (--r; r > Q[i].r; r--) del(c[r]);
- for (--l; l >= Q[i].l; l--) add(c[l]);
- for (++l; l < Q[i].l; l++) del(c[l]);
- nal[Q[i].i] = ans;
- }
- for (int i = 0; i < c1; i++) printf("%d\n", nal[i]);
- return 0;
- }
- ```
-
-## 树上莫队
-
-一般的莫队只能处理线性问题,我们要把树强行压成序列。
-
-我们可以将树的括号序跑下来,把括号序分块,在括号序上跑莫队。
-
-具体怎么做呢?
-
-dfs 一棵树,然后如果 dfs 到 x 点,就 `push_back(x)` ,dfs 完 x 点,就直接 `push_back(-x)` ,然后我们在挪动指针的时候,
-
-- 新加入的值是 x ---> `add(x)`
-- 新加入的值是 - x ---> `del(x)`
-- 新删除的值是 x ---> `del(x)`
-- 新删除的值是 - x ---> `add(x)`
-
-这样的话,我们就把一棵树处理成了序列。
-
-???+note "例题[「WC2013」糖果公园](http://uoj.ac/problem/58)"
- 题意:给你一棵树,每个点有颜色,每次询问
-
- $$\sum_{c}val_c\sum_{i=1}^{cnt_c}w_i$$
-
- 其中:$val$ 表示该颜色的价值,$cnt$ 表示颜色出现的次数,$w$ 表示该颜色出现 $i$ 次后的价值
-
-先把树变成序列,然后每次添加/删除一个点,这个点的对答案的的贡献是可以在 $O(1)$ 时间内获得的,即 $val_c\times w_{cnt_{c+1}}$
-
-发现因为他会把起点的子树也扫了一遍,产生多余的贡献,怎么办呢?
-
-因为扫的过程中起点的子树里的点肯定会被扫两次,但贡献为 0。
-
-所以可以开一个 $vis$ 数组,每次扫到点 x,就把 $vis_x$ 异或上 1。
-
-如果 $vis_x=0$ ,那这个点的贡献就可以不计。
-
-所以可以用树上莫队来求。
-
-修改的话,加上一维时间维即可,变成带修改树上莫队。
-
-然后因为所包含的区间内可能没有 LCA,对于没有的情况要将多余的贡献删除,然后就完事了。
-
-??? 参考代码
- ```cpp
- #include <algorithm>
- #include <cmath>
- #include <cstdio>
- #include <iostream>
-
- #define DEBUG printf("line:%d func:%s\n", __LINE__, __FUNCTION__);
-
- using namespace std;
-
- const int maxn = 200010;
-
- int f[maxn], g[maxn], id[maxn], head[maxn], cnt, last[maxn], dep[maxn],
- fa[maxn][22], v[maxn], w[maxn];
- int block, index, n, m, q;
- int pos[maxn], col[maxn], app[maxn];
- bool vis[maxn];
- long long ans[maxn], cur;
-
- struct edge {
- int to, nxt;
- } e[maxn];
- int cnt1 = 0, cnt2 = 0; // 时间戳
-
- struct query {
- int l, r, t, id;
- bool operator<(const query &b) const {
- return (pos[l] < pos[b.l]) || (pos[l] == pos[b.l] && pos[r] < pos[b.r]) ||
- (pos[l] == pos[b.l] && pos[r] == pos[b.r] && t < b.t);
- }
- } a[maxn], b[maxn];
-
- inline void addedge(int x, int y) {
- e[++cnt] = (edge){y, head[x]};
- head[x] = cnt;
- }
-
- void dfs(int x) {
- id[f[x] = ++index] = x;
- for (int i = head[x]; i; i = e[i].nxt) {
- if (e[i].to != fa[x][0]) {
- fa[e[i].to][0] = x;
- dep[e[i].to] = dep[x] + 1;
- dfs(e[i].to);
- }
- }
- id[g[x] = ++index] = x; // 括号序
- }
-
- inline void swap(int &x, int &y) {
- int t;
- t = x;
- x = y;
- y = t;
- }
-
- inline int lca(int x, int y) {
- if (dep[x] < dep[y]) swap(x, y);
- if (dep[x] != dep[y]) {
- int dis = dep[x] - dep[y];
- for (int i = 20; i >= 0; i--)
- if (dis >= (1 << i)) dis -= 1 << i, x = fa[x][i];
- } // 爬到同一高度
- if (x == y) return x;
- for (int i = 20; i >= 0; i--) {
- if (fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
- }
- return fa[x][0];
- }
-
- inline void add(int x) {
- if (vis[x])
- cur -= (long long)v[col[x]] * w[app[col[x]]--];
- else
- cur += (long long)v[col[x]] * w[++app[col[x]]];
- vis[x] ^= 1;
- }
-
- inline void modify(int x, int t) {
- if (vis[x]) {
- add(x);
- col[x] = t;
- add(x);
- } else
- col[x] = t;
- } // 在时间维上移动
-
- int main() {
- scanf("%d%d%d", &n, &m, &q);
- for (int i = 1; i <= m; i++) scanf("%d", &v[i]);
- for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
- for (int i = 1; i < n; i++) {
- int x, y;
- scanf("%d%d", &x, &y);
- addedge(x, y);
- addedge(y, x);
- }
- for (int i = 1; i <= n; i++) {
- scanf("%d", &last[i]);
- col[i] = last[i];
- }
- dfs(1);
- for (int j = 1; j <= 20; j++)
- for (int i = 1; i <= n; i++)
- fa[i][j] = fa[fa[i][j - 1]][j - 1]; // 预处理祖先
- int block = pow(index, 2.0 / 3);
- for (int i = 1; i <= index; i++) {
- pos[i] = (i - 1) / block;
- }
- while (q--) {
- int opt, x, y;
- scanf("%d%d%d", &opt, &x, &y);
- if (opt == 0) {
- b[++cnt2].l = x;
- b[cnt2].r = last[x];
- last[x] = b[cnt2].t = y;
- } else {
- if (f[x] > f[y]) swap(x, y);
- a[++cnt1] = (query){lca(x, y) == x ? f[x] : g[x], f[y], cnt2, cnt1};
- }
- }
- sort(a + 1, a + cnt1 + 1);
- int L, R, T; // 指针坐标
- L = R = 0;
- T = 1;
- for (int i = 1; i <= cnt1; i++) {
- while (T <= a[i].t) {
- modify(b[T].l, b[T].t);
- T++;
- }
- while (T > a[i].t) {
- modify(b[T].l, b[T].r);
- T--;
- }
- while (L > a[i].l) {
- L--;
- add(id[L]);
- }
- while (L < a[i].l) {
- add(id[L]);
- L++;
- }
- while (R > a[i].r) {
- add(id[R]);
- R--;
- }
- while (R < a[i].r) {
- R++;
- add(id[R]);
- }
- int x = id[L], y = id[R];
- int llca = lca(x, y);
- if (x != llca && y != llca) {
- add(llca);
- ans[a[i].id] = cur;
- add(llca);
- } else
- ans[a[i].id] = cur;
- }
- for (int i = 1; i <= cnt1; i++) {
- printf("%lld\n", ans[i]);
- }
- return 0;
- }
- ```
-
-## 真·树上莫队
-
-上面的树上莫队只是将树转化成了链,下面的才是真正的树上莫队。
-
-由于莫队相关的问题都是模板题,因此实现部分不做太多解释
-
-### 询问的排序
-
-首先我们知道莫队的是基于分块的算法,所以我们需要找到一种树上的分块方法来保证时间复杂度。
-
-条件:
-
-- 属于同一块的节点之间的距离不超过给定块的大小
-- 每个块中的节点不能太多也不能太少
-- 每个节点都要属于一个块
-- 编号相邻的块之间的距离不能太大
-
-了解了这些条件后,我们看到这样一道题 [「SCOI2005」王室联邦](https://loj.ac/problem/2152) 。
-
-在这道题的基础上我们只要保证最后一个条件就可以解决分块的问题了。
-
-!!! 思路
- 令 lim 为希望块的大小,首先,对于整个树 dfs,当子树的大小大于 lim 时,就将它们分在一块,容易想到:对于根,可能会剩下一些点,于是将这些点分在最后一个块里。
-
-做法:用栈维护当前节点作为父节点访问它的子节点,当从栈顶到父节点的距离大于希望块的大小时,弹出这部分元素分为一块,最后剩余的一块单独作为一块。
-
-最后的排序方法:若第一维时间戳大于第二维,交换它们,按第一维所属块为第一关键字,第二维时间戳为第二关键字排序。
-
-### 指针的移动
-
-容易想到,我们可以标记被计入答案的点,让指针直接向目标移动,同时取反路径上的点。
-
-但是,这样有一个问题,若指针一开始都在 x 上,显然 x 被标记,当两个指针向同一子节点移动(还有许多情况)时,x 应该不被标记,但实际情况是 x 被标记,因为两个指针分别标记了一次,抵消了。
-
-如何解决呢?
-
-有一个很显然的性质:这些点肯定是某些 LCA,因为 LCA 处才有可能被重复撤销导致撤销失败。
-
-所以我们每次不标记 LCA,到需要询问答案时再将 LCA 标记,然后再撤销。
-
-```cpp
-//取反路径上除LCA以外的所有节点
-void move(int x, int y) {
- if (dp[x] < dp[y]) swap(x, y);
- while (dp[x] > dp[y]) update(x), x = fa[x];
- while (x != y) update(x), update(y), x = fa[x], y = fa[y];
- // x!=y保证LCA没被取反
-}
-```
-
-对于求 LCA,我们可以用树剖,然后我们就可以把分块的步骤放到树剖的第一次 dfs 里面,时间戳也可以直接用第二次 dfs 的 dfs 序。
-
-```cpp
-int bl[100002], bls = 0; //属于的块,块的数量
-unsigned step; //块大小
-int fa[100002], dp[100002], hs[100002] = {0}, sz[100002] = {0};
-//父节点,深度,重儿子,大小
-stack<int> sta;
-void dfs1(int x) {
- sz[x] = 1;
- unsigned ss = sta.size();
- for (int i = head[x]; i; i = nxt[i])
- if (ver[i] != fa[x]) {
- fa[ver[i]] = x;
- dp[ver[i]] = dp[x] + 1;
- dfs1(ver[i]);
- sz[x] += sz[ver[i]];
- if (sz[ver[i]] > sz[hs[x]]) hs[x] = ver[i];
- if (sta.size() - ss >= step) {
- bls++;
- while (sta.size() != ss) bl[sta.top()] = bls, sta.pop();
- }
- }
- sta.push(x);
-}
-// main
-if (!sta.empty()) {
- bls++; //这一行可写可不写
- while (!sta.empty()) bl[sta.top()] = bls, sta.pop();
-}
-```
-
-### 时间复杂度
-
-重点到了,这里关系到块的大小取值。
-
-设块的大小为 $unit$ :
-
-- 对于 x 指针,由于每个块中节点的距离在 $unit$ 左右,每个块中 x 指针移动 $unit^2$ 次( $unit\times dis_max$ ),共计 $n\times unit$ ( $unit^2 \times (n\div unit)$ )次;
-- 对于 y 指针,每个块中最多移动 $O(n)$ 次,共计 $n^2\div unit$ ( $n \times (n \div unit)$ )次。
-
-加起来大概在根号处取得最小值(由于树上莫队块的大小不固定,所以不一定要严格按照)。
-
-### 例题「WC2013」糖果公园
-
-由于多了时间维,块的大小取到 $0.6n$ 的样子就差不多了。
-
-??? 参考代码
- ```cpp
- #include <bits/stdc++.h>
- //#pragma GCC optimize(2)
- using namespace std;
- inline int gi() {
- register int x, c, op = 1;
- while (c = getchar(), c < '0' || c > '9')
- if (c == '-') op = -op;
- x = c ^ 48;
- while (c = getchar(), c >= '0' && c <= '9')
- x = (x << 3) + (x << 1) + (c ^ 48);
- return x * op;
- }
- int head[100002], nxt[200004], ver[200004], tot = 0;
- void add(int x, int y) {
- ver[++tot] = y, nxt[tot] = head[x], head[x] = tot;
- ver[++tot] = x, nxt[tot] = head[y], head[y] = tot;
- }
- int bl[100002], bls = 0;
- unsigned step;
- int fa[100002], dp[100002], hs[100002] = {0}, sz[100002] = {0}, top[100002],
- id[100002];
- stack<int> sta;
- void dfs1(int x) {
- sz[x] = 1;
- unsigned ss = sta.size();
- for (int i = head[x]; i; i = nxt[i])
- if (ver[i] != fa[x]) {
- fa[ver[i]] = x, dp[ver[i]] = dp[x] + 1;
- dfs1(ver[i]);
- sz[x] += sz[ver[i]];
- if (sz[ver[i]] > sz[hs[x]]) hs[x] = ver[i];
- if (sta.size() - ss >= step) {
- bls++;
- while (sta.size() != ss) bl[sta.top()] = bls, sta.pop();
- }
- }
- sta.push(x);
- }
- int cnt = 0;
- void dfs2(int x, int hf) {
- top[x] = hf, id[x] = ++cnt;
- if (!hs[x]) return;
- dfs2(hs[x], hf);
- for (int i = head[x]; i; i = nxt[i])
- if (ver[i] != fa[x] && ver[i] != hs[x]) dfs2(ver[i], ver[i]);
- }
- int lca(int x, int y) {
- while (top[x] != top[y]) {
- if (dp[top[x]] < dp[top[y]]) swap(x, y);
- x = fa[top[x]];
- }
- return dp[x] < dp[y] ? x : y;
- }
- struct qu {
- int x, y, t, id;
- bool operator<(const qu a) const {
- return bl[x] == bl[a.x] ? (bl[y] == bl[a.y] ? t < a.t : bl[y] < bl[a.y])
- : bl[x] < bl[a.x];
- }
- } q[100001];
- int qs = 0;
- struct ch {
- int x, y, b;
- } upd[100001];
- int ups = 0;
- long long ans[100001];
- int b[100001] = {0};
- int a[100001];
- long long w[100001];
- long long v[100001];
- long long now = 0;
- bool vis[100001] = {0};
- void back(int t) {
- if (vis[upd[t].x]) {
- now -= w[b[upd[t].y]--] * v[upd[t].y];
- now += w[++b[upd[t].b]] * v[upd[t].b];
- }
- a[upd[t].x] = upd[t].b;
- }
- void change(int t) {
- if (vis[upd[t].x]) {
- now -= w[b[upd[t].b]--] * v[upd[t].b];
- now += w[++b[upd[t].y]] * v[upd[t].y];
- }
- a[upd[t].x] = upd[t].y;
- }
- void update(int x) {
- if (vis[x])
- now -= w[b[a[x]]--] * v[a[x]];
- else
- now += w[++b[a[x]]] * v[a[x]];
- vis[x] ^= 1;
- }
- void move(int x, int y) {
- if (dp[x] < dp[y]) swap(x, y);
- while (dp[x] > dp[y]) update(x), x = fa[x];
- while (x != y) update(x), update(y), x = fa[x], y = fa[y];
- }
- int main() {
- int n = gi(), m = gi(), k = gi();
- step = (int)pow(n, 0.6);
- for (int i = 1; i <= m; i++) v[i] = gi();
- for (int i = 1; i <= n; i++) w[i] = gi();
- for (int i = 1; i < n; i++) add(gi(), gi());
- for (int i = 1; i <= n; i++) a[i] = gi();
- for (int i = 1; i <= k; i++)
- if (gi())
- q[++qs].x = gi(), q[qs].y = gi(), q[qs].t = ups, q[qs].id = qs;
- else
- upd[++ups].x = gi(), upd[ups].y = gi();
- for (int i = 1; i <= ups; i++) upd[i].b = a[upd[i].x], a[upd[i].x] = upd[i].y;
- for (int i = ups; i; i--) back(i);
- fa[1] = 1;
- dfs1(1), dfs2(1, 1);
- if (!sta.empty()) {
- bls++;
- while (!sta.empty()) bl[sta.top()] = bls, sta.pop();
- }
- for (int i = 1; i <= n; i++)
- if (id[q[i].x] > id[q[i].y]) swap(q[i].x, q[i].y);
- sort(q + 1, q + qs + 1);
- int x = 1, y = 1, t = 0;
- for (int i = 1; i <= qs; i++) {
- if (x != q[i].x) move(x, q[i].x), x = q[i].x;
- if (y != q[i].y) move(y, q[i].y), y = q[i].y;
- int f = lca(x, y);
- update(f);
- while (t < q[i].t) change(++t);
- while (t > q[i].t) back(t--);
- ans[q[i].id] = now;
- update(f);
- }
- for (int i = 1; i <= qs; i++) printf("%lld\n", ans[i]);
- return 0;
- }
- ```
-
-## 回滚莫队
-
-有些题目在区间转移时,可能会出现增加或者删除无法实现的问题。在只有增加不可实现或者只有删除不可实现的时候,就可以使用回滚莫队在 $O(n \sqrt n)$ 的时间内解决问题。回滚莫队的核心思想就是既然我只能实现一个操作,那么我就只使用一个操作,剩下的交给回滚解决。
-
-回滚莫队分为只使用增加操作的回滚莫队和只使用删除操作的回滚莫队。以下仅介绍只使用增加操作的回滚莫队,只使用删除操作的回滚莫队和只使用增加操作的回滚莫队只在算法实现上有一点区别,故不再赘述。
-
-### 例题 [JOISC 2014 Day1 历史研究](https://loj.ac/problem/2874)
-
-给你一个长度为 $n$ 的数组 $A$ 和 $m$ 个询问 $(1 \leq n, m \leq 10^5)$ ,每次询问一个区间 $[L, R]$ 内重要度最大的数字,要求 **输出其重要度** 。一个数字 $i$ 重要度的定义为 $i$ 乘上 $i$ 在区间内出现的次数。
-
-在这个问题中,在增加的过程中更新答案是很好实现的,但是在删除的过程中更新答案是不好实现的。因为如果增加会影响答案,那么新答案必定是刚刚增加的数字的重要度,而如果删除过后区间重要度最大的数字改变,我们很难确定新的重要度最大的数字是哪一个。所以,普通的莫队很难解决这个问题。
-
-### 具体算法
-
-- 对原序列进行分块,对询问按以左端点所属块编号升序为第一关键字,右端点升序为第二关键字的方式排序
-- 按顺序处理询问
- - 如果询问左端点所属块 $B$ 和上一个询问左端点所属块的不同,那么将莫队区间的左端点初始化为 $B$ 的右端点加 1, 将莫队区间的右端点初始化为 $B$ 的右端点
- - 如果询问的左右端点所属的块相同,那么直接扫描区间回答询问
- - 如果询问的左右端点所属的块不同
- - 如果询问的右端点大于莫队区间的右端点,那么不断扩展右端点直至莫队区间的右端点等于询问的右端点
- - 不断扩展莫队区间的左端点直至莫队区间的左端点等于询问的左端点
- - 回答询问
- - 撤销莫队区间左端点的改动,使莫队区间的左端点回滚到 $B$ 的右端点加 1
-
-### 复杂度证明
-
-假设左右端点同属于一个块的询问个个数为 $C_1$ ,左右端点不属于同一个块的询问的个数为 $C_2$ 。
-
-回答一个左右端点同属于一个块的询问的时间复杂度为 $O(\sqrt n)$ ,回答所有左右端点同属于一个块的询问的时间复杂度为 $O(C_1\sqrt n)$ 。
-
-对于左右端点不属于同一个块的询问,将其按左端点所属块分类。对于一类询问,假设属于这一类询问的个数为 $c_i$ 。在回答这一类询问的时候莫队区间右端点至多扩展 $n$ 次;回答这一类问题中的一个的时候,左端点扩展和回滚的复杂度为 $O(\sqrt n)$ 。由此,回答一类问题的复杂度为 $O(n + c_i\sqrt n)$ 。总共有 $\sqrt n$ 类询问,所以回答左右端点不属于同一个块的询问的时间复杂度为 $O(C_2\sqrt n + n\sqrt n)$ 。
-
-综上,这个算法的复杂度 $T(n) = O(C_2\sqrt n + n\sqrt n) + O(C_1\sqrt n) = O(n\sqrt n + m\sqrt n)$ 。
-
-??? 参考代码
- ```cpp
- #include <bits/stdc++.h>
- using namespace std;
-
- typedef long long ll;
- const int N = 1e5 + 5;
- int n, q;
- int x[N], t[N], m;
-
- struct Query {
- int l, r, id;
- } Q[N];
- int pos[N], L[N], R[N], sz, tot;
- int cnt[N], __cnt[N];
- ll ans[N];
-
- inline bool cmp(const Query& A, const Query& B) {
- if (pos[A.l] == pos[B.l]) return A.r < B.r;
- return pos[A.l] < pos[B.l];
- }
-
- void build() {
- sz = sqrt(n);
- tot = n / sz;
- for (int i = 1; i <= tot; i++) {
- L[i] = (i - 1) * sz + 1;
- R[i] = i * sz;
- }
- if (R[tot] < n) {
- ++tot;
- L[tot] = R[tot - 1] + 1;
- R[tot] = n;
- }
- }
-
- inline void Add(int v, ll& Ans) {
- ++cnt[v];
- Ans = max(Ans, 1LL * cnt[v] * t[v]);
- }
-
- inline void Del(int v) { --cnt[v]; }
-
- int main() {
- scanf("%d %d", &n, &q);
- for (int i = 1; i <= n; i++) scanf("%d", &x[i]), t[++m] = x[i];
- for (int i = 1; i <= q; i++) scanf("%d %d", &Q[i].l, &Q[i].r), Q[i].id = i;
-
- build();
-
- // 对询问进行排序
- for (int i = 1; i <= tot; i++)
- for (int j = L[i]; j <= R[i]; j++) pos[j] = i;
- sort(Q + 1, Q + 1 + q, cmp);
-
- // 离散化
- sort(t + 1, t + 1 + m);
- m = unique(t + 1, t + 1 + m) - (t + 1);
- for (int i = 1; i <= n; i++) x[i] = lower_bound(t + 1, t + 1 + m, x[i]) - t;
-
- int l = 1, r = 0, last_block = 0, __l;
- ll Ans = 0, tmp;
- for (int i = 1; i <= q; i++) {
- // 询问的左右端点同属于一个块则暴力扫描回答
- if (pos[Q[i].l] == pos[Q[i].r]) {
- for (int j = Q[i].l; j <= Q[i].r; j++) ++__cnt[x[j]];
- for (int j = Q[i].l; j <= Q[i].r; j++)
- ans[Q[i].id] = max(ans[Q[i].id], 1LL * t[x[j]] * __cnt[x[j]]);
- for (int j = Q[i].l; j <= Q[i].r; j++) --__cnt[x[j]];
- continue;
- }
-
- // 访问到了新的块则重新初始化莫队区间
- if (pos[Q[i].l] != last_block) {
- while (r > R[pos[Q[i].l]]) Del(x[r]), --r;
- while (l < R[pos[Q[i].l]] + 1) Del(x[l]), ++l;
- Ans = 0;
- last_block = pos[Q[i].l];
- }
-
- // 扩展右端点
- while (r < Q[i].r) ++r, Add(x[r], Ans);
- __l = l;
- tmp = Ans;
-
- // 扩展左端点
- while (__l > Q[i].l) --__l, Add(x[__l], tmp);
- ans[Q[i].id] = tmp;
-
- // 回滚
- while (__l < l) Del(x[__l]), ++__l;
- }
- for (int i = 1; i <= q; i++) printf("%lld\n", ans[i]);
- return 0;
- }
- ```
-
-## 莫队配合 bitset
-
-bitset 常用于常规数据结构难以维护的的判定、统计问题,而莫队可以维护常规数据结构难以维护的区间信息。把两者结合起来使用可以同时利用两者的优势。
-
-### 例题 [「Ynoi2016」掉进兔子洞](https://www.luogu.com.cn/problem/P4688)
-
-本题刚好符合上面提到的莫队配合 bitset 的特征。不难想到我们可以分别用 bitset 存储每一个区间内的出现过的所有权值,一组询问的答案即所有区间的长度和减去三者的并集元素个数 $\times 3$ 。
-
-但是在莫队中使用 bitset 也需要针对 bitset 的特性调整算法:
-
-1. bitset 不能很好地处理同时出现多个权值的情况。我们可以把当前元素离散化后的权值与当前区间的的出现次数之和作为往 bitset 中插入的对象。
-2. 我们平常使用莫队时,可能会不注意 4 种移动指针的方法顺序,所以指针移动的过程中可能会出现区间的左端点在右端点右边,区间长度为负值的情况,导致元素的个数为负数。这在其他情况下并没有什么影响,但是本题中在 bitset 中插入的元素与元素个数有关,所以我们需要注意 4 种移动指针的方法顺序,将左右指针分别往左边和右边移动的语句写在前面,避免往 bitset 中插入负数。
-3. 虽然 bitset 用空间小,但是仍然难以承受 $10 ^ 5 \times 10 ^ 5$ 的数据规模。所以我们需要将询问划分成常数块分别处理,保证空间刚好足够的情况下时间复杂度不变。
-
-??? 参考代码
- ```cpp
- #include <algorithm>
- #include <bitset>
- #include <cmath>
- #include <cstdio>
- #include <cstring>
- using namespace std;
- const int N = 100005, M = N / 3 + 10;
- int n, m, maxn;
- int a[N], ans[M], cnt[N];
- bitset<N> sum[M], now;
- struct query {
- int l, r, id;
- bool operator<(const query& x) const {
- if (l / maxn != x.l / maxn) return l < x.l;
- return (l / maxn) & 1 ? r < x.r : r > x.r;
- }
- } q[M * 3];
- void static_set() {
- static int tmp[N];
- memcpy(tmp, a, sizeof(a));
- sort(tmp + 1, tmp + n + 1);
- for (int i = 1; i <= n; i++)
- a[i] = lower_bound(tmp + 1, tmp + n + 1, a[i]) - tmp;
- }
- void add(int x) {
- now.set(x + cnt[x]);
- cnt[x]++;
- }
- void del(int x) {
- cnt[x]--;
- now.reset(x + cnt[x]);
- }
- void solve() {
- int cnt = 0, tot = 0;
- now.reset();
- for (tot = 0; tot < M - 5 && m; tot++) {
- m--;
- ans[tot] = 0;
- sum[tot].set();
- for (int j = 0; j < 3; j++) {
- scanf("%d%d", &q[cnt].l, &q[cnt].r);
- q[cnt].id = tot;
- ans[tot] += q[cnt].r - q[cnt].l + 1;
- cnt++;
- }
- }
- sort(q, q + cnt);
- for (int i = 0, l = 1, r = 0; i < cnt; i++) {
- while (l > q[i].l) add(a[--l]);
- while (r < q[i].r) add(a[++r]);
- while (l < q[i].l) del(a[l++]);
- while (r > q[i].r) del(a[r--]);
- sum[q[i].id] &= now;
- }
- for (int i = 0; i < tot; i++)
- printf("%d\n", ans[i] - (int)sum[i].count() * 3);
- }
- int main() {
- scanf("%d%d", &n, &m);
- for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
- static_set();
- maxn = sqrt(n);
- solve();
- memset(cnt, 0, sizeof(cnt));
- solve();
- memset(cnt, 0, sizeof(cnt));
- solve();
- return 0;
- }
- ```
-
-### 习题
-
-- [小清新人渣的本愿](https://www.luogu.com.cn/problem/P3674)
-- [「Ynoi2017」由乃的玉米田](https://www.luogu.com.cn/problem/P5355)
-- [「Ynoi2011」WBLT](https://www.luogu.com.cn/problem/P5313)
-
## 参考资料
- - [莫队算法学习笔记 | Sengxian's Blog](https://blog.sengxian.com/algorithms/mo-s-algorithm)
-- [莫队算法学习笔记 | Sengxian's Blog](https://blog.sengxian.com/algorithms/mo-s-algorithm)
-- [回滚莫队及其简单运用 | Parsnip's Blog](https://www.cnblogs.com/Parsnip/p/10969989.html)
++- [莫队算法学习笔记 | Sengxian's Blog](https://blog.sengxian.com/algorithms/mo-s-algorithm)