// // // // // // // // // // // // // //

关于莫队

关于莫队

前言

莫队 众所周知 一种暴力

虽然没有学完 但是已经学了一些了 想到再不写一下可能就没时间写了 然后开始写这个


文中默认 \(n\)\(m\) 同阶

另外 文中并不涉及关于详细的复杂度分析或者引入啊之类的东西 反正也没人看


标点符号是不可能的 这辈子都不可能有标点符号的


莫队

莫队可以处理一系列的区间询问问题 首先对询问离线 通过对于询问进行合理的排序 暴力移动区间的左右端点 快速的更新答案

说人话大概就是如果已经有了区间 \([l, r]\) 的答案 我们可以非常方便的扩展到 \([l, r - 1], [l, r + 1], [l - 1, r], [l + 1, r]\) 那么可以在 \(O(n\sqrt n)\) 的复杂度内求解

流程

  1. 将原序列分块
    块的大小为 \(\sqrt n\)
  2. 将询问离线并进行排序
    将询问离线后进行排序 排序时以左端点所在块为第一关键词 右端点所在块为第二关键词 将左端点在同一个块中的询问按照右端点升序排列 否则按照左端点所在块升序排列
  3. 暴力移动左右端点 以某种方式 \(O(1)\) 的扩展区间的答案
    这里更新答案的方式依照题目而定
  4. 最后将答案一次性输出

关于复杂度

分完块之后每个块的大小是 \(\sqrt n\) 的 左端点每一次最多移动 \(\sqrt n\) 的距离 最多移动 \(n\) 次 右端点单调不降 每一个块移动 \(n\) 的距离移动 \(\sqrt n\) 次 总复杂度为 \(O(n\sqrt n)\)

为什么块的大小是 \(\sqrt n\)

\(m\)\(n\) 同阶时 块的大小是 \(\sqrt n\) 时理论复杂度是最优的

设块的大小为 \(k\) 我们将左端点在同一个块中的询问放到了一起 每次询问中 在一个块中左端点最多移动 \(k\) 次 有 \(m\) 个询问 所以复杂度为 \(O(mk)\)

右端点是单调递增的 最多移动 \(n\) 次 有 \(\frac nk\) 个块 所以复杂度为 \(O(\frac {n^2}k)\)

总的复杂度为 \(O(mk + \frac {n ^ 2}k)\)

块的大小是自己定的 当 \(mk = \frac {n ^ 2}k\)\(k = \sqrt {\frac {n^2}m}\)\(mk + \frac {n ^ 2}k\) 最小 \(n\)\(m\) 同阶 \(k = \sqrt n\)

代码的话照着题目看吧

题目

P2709 小B的询问

给定长为 \(n\) 的序列 \(m\) 个询问 每次询问给定区间 \([l, r]\) 求 $$\sum_{i = l}^r c_i^2$$ \(c_i\) 表示数字 \(i\) 在区间中出现的次数

普通莫队 完全平方公式展一下就可以直接搞了 下面是代码

/*
  Time: 12.12
  Worker: Blank_space
  Source: P2709 小B的询问
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<set>
#define int long long
#define emm(x) memset(x,0,sizeof x)
#define emmm(x) memset(x,0x3f,sizeof x)
using namespace std;
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int C = 1e6 + 7;
const int D = 1e7 + 7;
/*------------------------------------常量定义*/
int n, m, k, pos[B], a[B], sum[B], ans, _ans[B];
struct node{
	int l, r, id;
	bool operator < (const node & x) const
	{
		if(pos[l] == pos[x.l]) return r < x.r;
		else return l < x.l;
	}
}q[B];
/*------------------------------------变量定义*/
inline int read(){
   int x = 0,f = 1;char ch = getchar();
   while(ch < '0' || ch > '9'){if(ch == '-') f=-1; ch = getchar();}
   while(ch >= '0' && ch <= '9'){x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
   return x * f;
}
/*----------------------------------------快读*/
void del(int x)
{
	ans += 1 - sum[a[x]] * 2;
	sum[a[x]]--;
}
void add(int x)
{
	ans += 1 + sum[a[x]] * 2;
	sum[a[x]]++;
}
/*----------------------------------------函数*/
signed main()
{
    n = read(); m = read(); k = read(); int _k = sqrt(n), l = 1, r = 0;
    for(int i = 1; i <= n; i++) a[i] = read();
    for(int i = 1; i <= n; i++) pos[i] = (i - 1) / _k + 1;
    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);
    for(int i = 1; i <= m; i++)
    {
    	while(l < q[i].l) del(l++);
    	while(l > q[i].l) add(--l);
    	while(r < q[i].r) add(++r);
    	while(r > q[i].r) del(r--);
    	_ans[q[i].id] = ans;
    }
    for(int i = 1; i <= m; i++) printf("%lld\n", _ans[i]);
	return 0;
}

(\(ps\): 你可以看出代码是上古时代的 后来码风有过几次变异越变越诡异 可能多有不同

一点常数优化

从代码以及上面不难看出 在每一个块搞的时候 \(r\) 指针会从左边蹦到最右边 在搞完一个块之后 搞下一个块之前一定会再蹦回最左端 所以排序的时候可以按照询问左端点所在块的奇偶性排序 奇数的话将 \(r\) 升序排列 偶数降序排列

算法优劣

优点

复杂度比较优秀 可以解决一类区间问题

思维难度小 代码框架简单

缺点

只能离线 遇上强制在线的就委了

复杂度终究是 \(O(n\sqrt n)\) 的 无法处理数据太大的问题 而且所能解决的问题也比较有局限性

其他题目


再来看个题

给定长度为 \(n\) 的序列 \(m\) 次操作 每次操作为区间 \([l, r]\) 求给定区间内出现的数的个数 (此时你发现这个题比上面那个例题还要水) 或者将某一位置 \(pos\) 的值修改为 \(val\) (此时你发现莫队不行了)

如果没有修改操作的话 这是个比板子还要板子的板子题 但是加上修改... 就成了一道毒瘤题

这就需要莫队的扩展——带修莫队

带修莫队

题目

P1903 数颜色 / 维护队列

给定长度为 \(n\) 的序列 \(m\) 次操作 每次操作为区间 \([l, r]\) 求给定区间内出现的数的个数 或者将某一位置 \(pos\) 的值修改为 \(val\)

其实就是上面那个

带修莫队是支持单点修改的 一般也认为只支持单点修改

处理的方法是在普通莫队的基础上加上一维时间 同样进行排序再处理

查询的操作这里就略过了 只说修改

修改的顺序是不会变的 所以在每一个询问上面打上一个时间戳 表示在这次询问之前有过多少次修改

排序时在原来排序的基础上当询问的右端点在同一块中时按照时间升序排序

当进行修改时 我们将原数的贡献删掉 将要改为的数的贡献加入即可

可以参照这个题的代码理解

代码

/*
  Time: 6.30
  Worker: Blank_space
  Source: P1903 [国家集训队]数颜色 / 维护队列
*/
/*--------------------------------------------*/
#include<cmath>
#include<cstdio>
#include<algorithm>
#define Swap(x, y) ((x) ^= (y) ^= (x) ^= (y))
/*--------------------------------------头文件*/
const int B = 2e5 + 7;
const int C = 1e6 + 7;
/*------------------------------------常量定义*/
int n, m, a[B], pos[B], qcnt, ccnt, sum[C], cnt, ans[B];
char s[10];
struct Query {int l, r, T, id;} q[B];
struct Chenge {int pos, val;} c[B];
/*------------------------------------变量定义*/
inline int read() {
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
	return x * f;
}
/*----------------------------------------快读*/
bool cmp(Query x, Query y) {return pos[x.l] == pos[y.l] ? pos[x.r] == pos[y.r] ? x.T < y.T : (pos[x.l] & 1) ? x.r < y.r : x.r > y.r : x.l < y.l;}
void add(int x) {if(++sum[a[x]] == 1) cnt++;}
void del(int x) {if(--sum[a[x]] == 0) cnt--;}
void work(int t, int i) {
	if(q[i].l <= c[t].pos && c[t].pos <= q[i].r)
	{
		if(--sum[a[c[t].pos]] == 0) cnt--;
		if(++sum[c[t].val] == 1) cnt++;
	}
	Swap(c[t].val, a[c[t].pos]);
}
/*----------------------------------------函数*/
int main() {
	n = read(); m = read(); int k = pow(n, 2.0 / 3.0), l = 1, r = 0, T = 0;
	for(int i = 1; i <= n; i++) a[i] = read(), pos[i] = (i - 1) / k + 1;
	for(int i = 1; i <= m; i++)
	{
		scanf("%s", s + 1);
		if(s[1] == 'Q') q[++qcnt] = (Query){read(), read(), ccnt, qcnt};
		if(s[1] == 'R') c[++ccnt] = (Chenge){read(), read()};
	}
	std::sort(q + 1, q + 1 + qcnt, cmp);
	for(int i = 1; i <= m; i++)
	{
		while(l < q[i].l) del(l++);
		while(l > q[i].l) add(--l);
		while(r < q[i].r) add(++r);
		while(r > q[i].r) del(r--);
		while(T < q[i].T) work(++T, i);
		while(T > q[i].T) work(T--, i);
		ans[q[i].id] = cnt;
	}
	for(int i = 1; i <= qcnt; i++) printf("%d\n", ans[i]);
	return 0;
}

复杂度的话

\(k = n^{\frac 23}\) 时最优 为 \(O(n^{\frac 53})\) (至少比暴力好

详细的分析不再写了 (

算法优劣

优点

支持单点修改操作

缺点

复杂度高 容易歇逼

只能支持单点修改

其他题目

CF940F Machine Learning (比较暴力


再来一个题目

给定一棵 \(n\) 个点的树 \(m\) 次询问 每次询问点对 \(u, v\) 之间不同的颜色数量

只看后半句的话 很明显的莫队模板 但是这是在树上 所以需要让莫队上树

树上莫队

首先需要知道欧拉序和 \(LCA\) 下面会用到 这里不说了

下面 \(st\) 表示进入时的时间戳 \(ed\) 表示退出时的时间戳

题目

SP10707 COT2 - Count on a tree II

就是上面那个东西

以点对 \(u, v\) 为例(默认 \(u\)\(dfs\) 序比 \(u\) 小 分两种情况讨论 (由于没有画图的东西 图就不给了 直接说操作了

  1. \(u\)\(v\) 的子树中
    将询问区间记为 \([st_u, st_v]\) 对于区间中的数 在第一次出现时累加贡献 第二次出现时删除贡献 通过欧拉序定义 不难发现 出现两次的点一定不再路径中

  2. \(u\)\(v\) 没有祖孙关系
    取区间 \([ed_u, st_v]\) 同样是一次添加两次删除 我们发现这样并没有统计 \(lca\) 的贡献 我们单独记出来加上就好

最后注意一下数组的长度 这样这道题就完成了

\(LCA\) 怎么求都行 个人比较喜欢树剖

下面是代码

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#define pt putchar(' ')
#define pn putchar('\n')
#define Abs(x) ((x) < 0 ? -(x) : (x))
#define Min(x, y) ((x) < (y) ? (x) : (y))
#define Max(x, y) ((x) > (y) ? (x) : (y))
#define Swap(x, y) ((x) ^= (y) ^= (x) ^= (y))
/*---------------------------------------------------------------------*/
const int A = 1e5 + 7;
const int mod = 1e9 + 7;
/*---------------------------------------------------------------------*/
void Fire() {
	freopen(".in", "r", stdin);
	freopen(".out", "w", stdout);
}
/*---------------------------------------------------------------------*/
int n, m, b[A], st[A], ed[A], siz[A], dep[A], cnt, son[A], fa[A], top[A], pos[A], id[A], a[A], sum[A], ans[A], Ans;
struct Query {int l, r, id, lca;} q[A];
struct edge {int v, nxt;} e[A << 1];
int head[A], ecnt;
bool vis[A];
/*---------------------------------------------------------------------*/
inline int read() {
	int x = 0, f = 0; char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = 1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
	return f ? -x : x;
}
void print(int x) {if(x > 9) print(x / 10); putchar(x % 10 ^ 48);}
void Print(int x) {if(x < 0) putchar('-'), x = -x; print(x);}
/*---------------------------------------------------------------------*/
void add_edge(int u, int v) {e[++ecnt] = (edge){v, head[u]}; head[u] = ecnt;}
bool cmp(Query x, Query y) {return id[x.l] == id[y.l] ? (id[x.l] & 1) ? x.r < y.r : x.r > y.r : x.l < y.l;}
void dfs1(int u, int pre) {
	fa[u] = pre; dep[u] = dep[pre] + 1; siz[u] = 1; st[u] = ++cnt; pos[cnt] = u;
	for(int i = head[u], v; i; i = e[i].nxt)
	{
		if((v = e[i].v) == pre) continue;
		dfs1(v, u); siz[u] += siz[v];
		if(siz[v] > siz[son[u]]) son[u] = v;
	}
	ed[u] = ++cnt; pos[cnt] = u;
}
void dfs2(int u, int tp) {
	top[u] = tp; if(!son[u]) return ; dfs2(son[u], tp);
	for(int i = head[u], v; i; i = e[i].nxt) if((v = e[i].v) != fa[u] && v != son[u]) dfs2(v, v);
}
int LCA(int x, int y) {
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]]) Swap(x, y);
		x = fa[top[x]];
	}
	return dep[x] < dep[y] ? x : y;
}
void add(int x) {if(++sum[x] == 1) Ans++;}
void del(int x) {if(--sum[x] == 0) Ans--;}
void work(int x) {vis[x] ? del(a[x]) : add(a[x]); vis[x] ^= 1;}
/*---------------------------------------------------------------------*/
int main() {
	n = read(); m = read(); int k = sqrt(n), l = 1, r = 0;
	for(int i = 1; i ^ n + 1; i++) a[i] = b[i] = read();
	std::sort(b + 1, b + 1 + n); int kcnt = std::unique(b + 1, b + 1 + n) - b - 1;
	for(int i = 1; i ^ n + 1; i++) a[i] = std::lower_bound(b + 1, b + 1 + kcnt, a[i]) - b;
	for(int i = 1; i ^ (n << 1) + 1; i++) id[i] = (i - 1) / k + 1;
	for(int i = 1; i ^ n; i++)
	{
		int x = read(), y = read();
		add_edge(x, y); add_edge(y, x);
	}
	dfs1(1, 0); dfs2(1, 1);
	for(int i = 1; i ^ m + 1; i++)
	{
		int x = read(), y = read(), lca = LCA(x, y);
		if(st[x] > st[y]) Swap(x, y);
		if(lca == x) q[i] = (Query){st[x], st[y], i, 0};
		else q[i] = (Query){ed[x], st[y], i, lca};
	}
	std::sort(q + 1, q + 1 + m, cmp);
	for(int i = 1; i ^ m + 1; i++)
	{
		while(l < q[i].l) work(pos[l++]);
		while(l > q[i].l) work(pos[--l]);
		while(r < q[i].r) work(pos[++r]);
		while(r > q[i].r) work(pos[r--]);
		if(q[i].lca) work(q[i].lca);
		ans[q[i].id] = Ans;
		if(q[i].lca) work(q[i].lca);
	}
	for(int i = 1; i ^ m + 1; i++) Print(ans[i]), pn;
	return 0;
}


其他题目

P4074 糖果公园 (树上带修莫队 据说这题原来是黑的 当然那是以前


又一道题目(可以预感到又一个新的莫队

给定长度 \(n\) 的序列 \(m\) 次询问 \([l, r]\) 求区间中最小没有出现过的自然数

其实这个题目在上面已经出现过了 \(CF940F\) 只不过那个题是有修改操作的 如果把修改操作去掉(再卡一下复杂度)就是上面这个题面了

对于删除操作 维护每个数出现的次数 在每一次删除后更新答案即可

对于添加操作 ... 好像并没有什么很好的方法保证优秀的复杂度

那么能不能只删除不添加呢?

是可以的 就在下面

回滚莫队

或者是 不删除/不添加莫队

当删除或添加中其中一个易于实现而另一个复杂度非常高的时候 我们可以考虑回滚莫队 只维护其中的一个操作

算法流程

下面以只增不删为例(另一种也类似)

分块 记录每个块的左右端点 将询问离线 排序 然后处理

如果询问的左右端点都在同一个块中 直接暴力求解

否则 在一个块中 将 \(l\) 指针掰到块的右端点的下一个位置 \(r\) 指针掰到右端点 对在该块中的询问 向右扩展 \(r\) 指针 不断添加 扩展完毕后将答案记下来 再向左暴力扩展 \(l\) 指针 同样是不断添加 扩展完毕后统计答案 同时利用之前记下的答案将 \(l\) 指针掰回原位置 再处理下一个询问

在一个块中的询问处理完后 将记录的数据清空 再处理下一个块

题目

P4137 Rmq Problem / mex

给定长度 \(n\) 的序列 \(m\) 次询问 \([l, r]\) 求区间中最小没有出现过的自然数

你发现这就是上面那个题目

模板题了 下面给代码

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#define pt putchar(' ')
#define pn putchar('\n')
#define Abs(x) ((x) < 0 ? -(x) : (x))
#define Min(x, y) ((x) < (y) ? (x) : (y))
#define Max(x, y) ((x) > (y) ? (x) : (y))
#define Swap(x, y) ((x) ^= (y) ^= (x) ^= (y))
/*-----------------------------------------------------------------------*/
const int B = 2e5 + 7;
const int mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
/*-----------------------------------------------------------------------*/
void File() {
	freopen(".in", "r", stdin);
	freopen(".out", "w", stdout);
}
/*-----------------------------------------------------------------------*/
int n, m, cnt[B], tmp[B], a[B], id[B], t, L[B], R[B], ans[B];
struct Query {int l, r, id;} q[B];
/*-----------------------------------------------------------------------*/
inline int read() {
	int x = 0, f = 0; char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = 1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
	return f ? -x : x;
}
void print(int x) {if(x > 9) print(x / 10); putchar(x % 10 ^ 48);}
void Print(int x) {if(x < 0) putchar('-'), x = -x; print(x);}
/*-----------------------------------------------------------------------*/
bool cmp(Query x, Query y) {return id[x.l] ^ id[y.l] ? id[x.l] < id[y.l] : x.r > y.r;}
void up_date(int i, int &y) {
	y = 0;
	for(int j = q[i].l; j ^ q[i].r + 1; j++) ++tmp[a[j]];
	while(tmp[y]) y++; ans[q[i].id] = y;
	for(int j = q[i].l; j ^ q[i].r + 1; j++) --tmp[a[j]];
}
void del(int x, int &y) {if(!--cnt[x]) y = Min(y, x);}
/*-----------------------------------------------------------------------*/
int main() {
	n = read(); m = read(); int len = sqrt(n);
	for(int i = 1; i ^ n + 1; i++) a[i] = read(), cnt[a[i]]++, id[i] = (i - 1) / len + 1;
	for(int i = 1; i ^ id[n]; i++) L[i] = R[i - 1] + 1, R[i] = i * len;  L[id[n]] = R[id[n] - 1] + 1; R[id[n]] = n;
	for(int i = 1; i ^ m + 1; i++) q[i] = (Query){read(), read(), i};
	while(cnt[t]) t++; std::sort(q + 1, q + 1 + m, cmp);
	for(int i = 1, k = 1, l, r, tmp1, tmp2; k ^ id[n] + 1; k++)
	{
		l = L[k]; r = n; tmp1 = t;
		for(; k == id[q[i].l]; i++)
		{
			if(id[q[i].r] == k) {up_date(i, tmp2); continue;}
			while(r > q[i].r) del(a[r--], tmp1);
			tmp2 = tmp1;
			while(l < q[i].l) del(a[l++], tmp2);
			ans[q[i].id] = tmp2;
			while(l > L[k]) cnt[a[--l]]++;
		}
		while(r < n) cnt[a[++r]]++;
		while(l < L[k + 1]) del(a[l++], t);
	}
	for(int i = 1; i ^ m + 1; i++) Print(ans[i]), pn;
	return 0;
}

它的复杂度是没有问题的 只是根据写法可能常数不同 比如在清空时如果频繁的使用 \(memset\) 可能就会导致超时

回滚莫队应该是可以代替普通的莫队的 (个人感觉

其他题目

P5906 【模板】回滚莫队&不删除莫队

AT1219 歴史の研究

SP20644 ZQUERY - Zero Query


又一道...

额 不是劝退 没有题目了

二次离线和在线莫队并没有学 所以后面当然是没有题目了

后面的咕了 学了再补充

posted @ 2021-07-13 16:54  Blank_space  阅读(54)  评论(0编辑  收藏  举报
// // // // // // //