优雅的暴力——莫队

发现莫队挺好玩的,写个学习笔记吧。

约定

在无特殊说明下,数据范围一律认为是 \(1\le n,Q\le10^5\)\(0\le a_i\le10^9\),其中 \(n\) 表示序列长度,\(Q\) 表示询问次数,\(a_i\) 表示序列中的元素。

莫队

莫队算法是一种可以解决大部分区间离线问题的离线算法,其主要思想是分块。因此大部分时候其复杂度为 \(O(n\sqrt n)\)

0.引入

经典例题 \(1\)

给出一个长度为 \(n\) 的正整数序列 \(A\),多次询问区间 \([l,r]\) 的数的和。

Solution

如果用常规方法,大家第一个想到的肯定是用前缀和,做到 \(O(n)\) 预处理,\(O(1)\) 查询。

现在我们要求不用前缀和 (就当我构造数据把前缀和卡了),用莫队怎么做?

假如说这个序列是 \(A=\{a_1,a_2,a_3,...,a_n\}\),如果我们已经知道区间 \([x,y]\) 的答案 \(ans\),我们如何求 \([x,y+1]\) 或者 \([x,y-1]\) 的答案呢?很简单,我们只需要让 \(ans\) 加上 \(a_{y+1}\) 或者让 \(ans\) 减去 \(a_y\) 就行了。\(x\) 的操作同理。

于是显而易见的,我们称一次让 \(ans\) 像这样变化的操作为一次“移动”,那么进行“移动”的其实就是区间的左端点或者右端点。而莫队就是进行这样的“移动”,来找到我们要求的答案的。

那么初始代码就是这样的:

int l = 1, r = 0;//需要“移动”的左指针、右指针
for (int i = 1; i <= Q; ++ i) {
    int ans = 0;//答案
    int ql, qr;//查询的区间
    cin >> ql >> qr;
    while (l < ql) ans -= a[l ++];
    while (l > ql) ans += a[-- l];
    while (r < qr) ans += a[++ r];
    while (r > qr) ans -= a[r --];//进行“移动”来统计答案
    cout << ans << endl;
}

容易发现,假设当前区间为 \([l,r]\),查询的区间是 \([ql,qr]\),每完成一轮“移动”所需要的复杂度是 \(O(|ql-l|+|qr-r|)=O(n)\) 的,如果说有 \(Q\) 次查询,相邻两个查询的左端点和右端点跨度很大,复杂度就是 \(O(nQ)\),这份代码就起飞了。

那怎么优化呢?

1.普通莫队

我们先将序列进行分块处理,然后将询问离线,并将询问所涉及的区间按照左端点所在的块为第一关键字,右端点为第二关键字排序。如果两个查询区间的左端点在同一块内,就将右端点按照从小到大排序,否则将左端点所在块的编号从小到大排序。

复杂度分析

设块长为 \(B\),由于询问是排过序的,所以左端点在同一块内移动次数不超过 \(O(B)\),跨块移动次数不超过 \(O(\frac{n}{B})\),右端点同理。那么每一轮移动的复杂度就从 \(O(n)\) 降到了 \(O(\max\{B,\frac{n}{B}\})\),显然最平衡的时候取 \(B=\sqrt n\),复杂度 \(O(\sqrt n)\),总复杂度为 \(O(Q\sqrt n)\),可以接受 \(n,Q\le10^5\) 的数据。

于是代码可以写成这个样子:

bool cmp (MO x, MO y) {
	return pos[x.l] == pos[y.l] ? x.r < y.r : pos[x.l] < pos[y.l];
}

int main () {
	read (n, m);
	int t = sqrt (n);
	for (int i = 1; i <= n; ++ i) read (a[i]), pos[i] = (i - 1) / t + 1;
	for (int i = 1; i <= m; ++ i) {
		read (q[i].l, q[i].r);
		q[i].id = i;
	}//询问离线
	sort (q + 1, q + m + 1, cmp);
	int l = 1, r = 0;
	for (int i = 1; i <= m; ++ i) {
		while (l > q[i].l) add (-- l);
		while (l < q[i].l) sub (l ++);
		while (r > q[i].r) sub (r --);
		while (r < q[i].r) add (++ r);//add 和 sub 就是一次“移动”
		ans[q[i].id] = res;//记录答案
	}
	for (int i = 1; i <= m; ++ i) print (ans[i], '\n');//输出
	return 0;
}

知道了这些,那么就来做一些题吧。


\(1\)P2709 【模板】莫队 / 小B的询问

小 B 有一个长为 \(n\) 的整数序列 \(a\),值域为 \([1,k]\)
他一共有 \(m\) 个询问,每个询问给定一个区间 \([l,r]\),求:

\[\sum\limits_{i=1}^k c_i^2 \]

其中 \(c_i\) 表示数字 \(i\)\([l,r]\) 中的出现次数。

小 B 请你帮助他回答询问。

对于 \(100\%\) 的数据,\(1\le n,m,k \le 5\times 10^4\)

Solution

可以发现,如果这个题用线段树之类,好像不是比较好维护。注意到数据范围比较小,可以考虑莫队。

那么这个莫队要维护什么内容呢?很显然,就是每一个数出现的次数。

对于一个区间 \([l,r]\),令 \(cnt_x\) 表示 \(x\) 这个数出现的次数,那么当我们进行“移动”的时候,我们对 \(cnt\) 进行相应的加或者减,然后统计答案就行了。

总复杂度为 \(O(n\sqrt n)\)

Code
#include<bits/stdc++.h>
#define int long long
#define rep(i,a,b) for(register int i=(a);i<=(b);++i)
#define per(i,a,b) for(register int i=(a);i>=(b);--i)
#define pi pair<int,int>
#define mkp(a,b) make_pair((a),(b)) 
#define IOS cin.tie(0)->sync_with_stdio(0);
using namespace std;
const int N = 1e6 + 15, M = 1e3 + 15;

const bool I_LOVE_CCF = true;

int n, m, k, t, res;
int pos[N];
int a[N], cnt[N];
int ans[N];
struct node {
	int l, r;
	int k;
}q[N];

inline void read (int &n) {
	int x = 0, f = 1;
	char ch = getchar ();
	while (!isdigit (ch)) {
		if (ch == '-') f = -1;
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ 48);
		ch = getchar ();
	}
	n = x * f;
}

bool cmp (node x, node y) {
	return pos[x.l] == pos[y.l]? x.r < y.r : pos[x.l] < pos[y.l];
}

void add (int x) {
	cnt[a[x]] ++;
	res += cnt[a[x]] * cnt[a[x]] - (cnt[a[x]] - 1) * (cnt[a[x]] - 1);
}

void sub (int x) {
	cnt[a[x]] --;
	res += cnt[a[x]] * cnt[a[x]] - (cnt[a[x]] + 1) * (cnt[a[x]] + 1);
}//“移动”并计算当前的答案

signed main () {
	read (n), read (m), read (k);
	t = sqrt (n);
	rep (i, 1, n) {
		cin >> a[i];
		pos[i] = (i - 1) / t + 1;
	}
	rep (i, 1, m) {
		read (q[i].l), read (q[i].r);
		q[i].k = i;
	}
	sort (q + 1, q + m + 1, cmp);//将询问排序
	int l = 1, r = 0;
	rep (i, 1, m) {
		while (l > q[i].l) add (-- l);
		while (l < q[i].l) sub (l ++);
		while (r > q[i].r) sub (r --);
		while (r < q[i].r) add (++ r);//“移动”区间
		ans[q[i].k] = res;//记录答案
	}
	rep (i, 1, m) printf ("%lld\n", ans[i]);
	return 0;
}

\(2\)P7764 [COCI 2016/2017 #5] Poklon

给定一个包含 \(N\) 个自然数的数组。

接着需要回答 \(Q\) 次询问,每次询问输出区间 \([L,R]\) 内恰好出现两次的自然数的数量。

对于 \(100\%\) 的数据,\(1 \le N,Q \le 5 \times 10^5\)\(1 \le L \le R \le N\),数组中的元素都是小于 \(10^9\) 的自然数。

Solution

和上一个题一样,线段树等数据结构很难维护恰好出现两次的数的数量,于是考虑莫队。

莫队维护的内容依旧是每个数出现的次数。我们在“移动”的时候,每扩充进来一个数,就给这个数出现的次数加上 \(1\),并判断它是不是恰好出现了两次。如果是,就将答案加 \(1\),否则就看这个数是不是从出现两次变过来的,是的话就减 \(1\),不是就不管。删除操作同理。

总复杂度依旧为 \(O(n\sqrt n)\)

Code
#include <bits/stdc++.h>
#define loop(i,a,b) for(int i=(a);~i;i=(b))
#define Mem(a,b) memset ((a),(b),sizeof((a)))
#define eb emplace_back
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N = 5e5 + 15;

namespace FAST_IO {
//快读快写已为您省略
}
using namespace FAST_IO;

int n, m;
int a[N];
int cnt[N], pos[N];
int res, ans[N];
struct MO {
	int l, r;
	int id;
}q[N];

bool cmp (MO x, MO y) {
	if (pos[x.l] == pos[y.l]) {
		if (pos[x.l] & 1) return x.r < y.r;
		else return x.r > y.r;
	} else return pos[x.l] < pos[y.l];
}

void add (int x) {
	++ cnt[a[x]];
	if (cnt[a[x]] == 2) ++ res;
	else if (cnt[a[x]] == 3) -- res;
}

void sub (int x) {
	-- cnt[a[x]];
	if (cnt[a[x]] == 2) ++ res;
	else if (cnt[a[x]] == 1) -- res;
}

int main () {
	read (n, m);
	int t = n / sqrt (m * 2.0 / 3);
	for (int i = 1; i <= n; ++ i) read (a[i]), pos[i] = (i - 1) / t + 1;
	for (int i = 1; i <= m; ++ i) {
		read (q[i].l, q[i].r);
		q[i].id = i;
	}
	sort (q + 1, q + m + 1, cmp);
	int l = 1, r = 0;
	for (int i = 1; i <= m; ++ i) {
		while (l > q[i].l) add (-- l);
		while (l < q[i].l) sub (l ++);
		while (r > q[i].r) sub (r --);
		while (r < q[i].r) add (++ r);
		ans[q[i].id] = res;
	}
	for (int i = 1; i <= m; ++ i) print (ans[i], '\n');
	return 0;
}

优化技巧

有些时候我们会遇到这样的数据:

1 1
2 1000
3 2
4 1000

按照原来的排序方法,可以算出右端点需要移动约 \(3000\) 次。但是我们是人,我们知道这样并不优,而最优的方法是处理完了\([2,1000]\) 这个区间的询问之后直接去处理 \([4,1000]\) 的询问,那这样我们只需要将左端点移动 \(2\) 次。

此时有一种排序方法叫做奇偶化排序,具体方法就是,对于每一块的奇偶性判断,对于奇数块的询问,将右端点从小到大进行排序,对于偶数块的询问,将右端点从大到小进行排序。

这样设计的原因是,右端点从左到右的移动是将加贡献,而从右到左的移动是减贡献,有些时候我们没必要将一部分贡献减出去,通过奇偶化排序可以确保右端点始终向一个方向移动,从而减少减贡献的移动次数。

代码长这样:

bool cmp (MO x, MO y) {
	if (pos[x.l] == pos[y.l]) {
		if (pos[x.l] & 1) return x.r < y.r;
		else return x.r > y.r;
	} else return pos[x.l] < pos[y.l];
}

2.带修莫队

我们刚刚看到的莫队都是不带修改的。现在我们来看以下问题。

经典例题 \(2\)

给出一个长度为 \(n\) 的正整数序列 \(A\),多次操作:

  1. 单点加。
  2. 查询区间 \([l,r]\) 的数的和。

Solution

就当线段树和树状数组做法被我卡了,用莫队解决该怎么办?

此时需要一种支持修改的莫队——带修莫队。

普通莫队我们只记录了查询的区间,带修莫队我们需要多记录一个次数,即在这个询问前经历了多少次修改。

我们仍然按照普通莫队的思路进行考虑,即:始终维护区间 \([l,r]\),通过每次移动一个点,扩充 \([l,r]\) 的范围,直到符合我们的询问范围为止。

我们仍然可以先进行扩充,当满足某个询问时,我们可以查询在这个询问之前经历过多少次的修改,对于每一次修改,如果是在当前询问的范围内,我们就进行修改的添加或撤销并更新贡献。

有一些细节要注意,就是带修莫队对查询进行排序的时候,要把左端点所在的块作为第一关键字,右端点所在的块作为第二关键字,次数作为第三关键字排序。

注意这类问题的块长为 \(n^{\frac{2}{3}}\)

那这个例题的主要代码就长这样:

bool cmp (MO x, MO y) {
	if (pos[x.l] ^ pos[y.l]) return pos[x.l] < pos[y.l];
	else if (pos[x.r] ^ pos[y.r]) return pos[x.r] < pos[y.r];
	else return x.tim < y.tim;
}//排序

void add (int x) {
	res += a[x];
}

void sub (int x) {
	res -= a[x];
}

int main () {
	read (n, m);
	int t = pow (n, 2.0 / 3);
	for (int i = 1; i <= n; ++ i) read (a[i]), pos[i] = (i - 1) / t + 1;
	for (int i = 1; i <= m; ++ i) {
		int op;
		read (op);
		if (op == 1) {
			int x, k;
			read (x, k);
			c[++ tc] = {x, k};//修改
		} else {
			int l, r;
			read (l, r);
			++ tq;
			q[tq] = {l, r, tc, tq};//询问
		}
	}
	sort (q + 1, q + tq + 1, cmp);
	int l = 1, r = 0, num = 0;
	for (int i = 1; i <= tq; ++ i) {
		while (l > q[i].l) add (-- l);
		while (l < q[i].l) sub (l ++);
		while (r > q[i].r) sub (r --);
		while (r < q[i].r) add (++ r);
		while (num < q[i].tim) {
			++ num;
			if (c[num].p >= l && c[num].p <= r) res += c[num].v;
			a[c[num].p] += c[num].v;
		}
		while (num > q[i].tim) {
			if (c[num].p >= l && c[num].p <= r) res -= c[num].v;
			a[c[num].p] -= c[num].v;
			num --;
		}
		ans[q[i].id] = res;
	}//带修莫队
	for (int i = 1; i <= tq; ++ i) print (ans[i], '\n');
	return 0;
}

时间复杂度为 \(O(n^{\frac{5}{3}})\)

\(3\)P1903 【模板】带修莫队 / [国家集训队] 数颜色 / 维护队列

墨墨购买了一套 \(N\) 支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:

  1. \(Q\ L\ R\) 代表询问你从第 \(L\) 支画笔到第 \(R\) 支画笔中共有几种不同颜色的画笔。

  2. \(R\ P\ C\) 把第 \(P\) 支画笔替换为颜色 \(C\)

为了满足墨墨的要求,你知道你需要干什么了吗?

对于所有数据,\(n,m \leq 133333\)

所有的输入数据中出现的所有整数均大于等于 \(1\) 且不超过 \(10^6\)

Solution

这就是一个单点改+莫队经典操作的题。考虑带修莫队。

和经典例题 \(2\) 一样,我们记录在每个查询前经历了多少次修改,然后按照普通莫队处理,维护每个数出现的次数,每处理完一个询问就进行修改,然后统计答案。

Code
#include<bits/stdc++.h>
#define int long long
#define rep(i,a,b) for(register int i=(a);i<=(b);++i)
#define per(i,a,b) for(register int i=(a);i>=(b);--i)
#define pi pair<int,int>
#define mkp(a,b) make_pair((a),(b)) 
#define IOS cin.tie(0)->sync_with_stdio(0);
using namespace std;
const int N = 1e6 + 15, M = 1e3 + 15;

const bool I_LOVE_CCF = true;

int n, m, t, ql, cl, res, num;
int pos[N], ans[N], cnt[N];
int a[N];
struct node {
	int l, r;
	int k, cnt;
}q[N];
struct node2 {
	int p, c;
}c[N];

inline void read (int &n) {
	int x = 0, f = 1;
	char ch = getchar ();
	while (!isdigit (ch)) {
		if (ch == '-') f = -1;
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ 48);
		ch = getchar ();
	}
	n = x * f;
}

bool cmp (node x, node y) {
	if (pos[x.l] != pos[y.l]) return pos[x.l] < pos[y.l];
	else if (pos[x.r] != pos[y.r]) return pos[x.r] < pos[y.r];
	else return x.cnt < y.cnt;
}

void add (int x) {
	cnt[a[x]] ++;
	if (cnt[a[x]] == 1) ++ res;
}

void sub (int x) {
	cnt[a[x]] --;
	if (cnt[a[x]] == 0) -- res;
}

void change (int l, int r, int num) {
	if (c[num].p >= l && c[num].p <= r) sub (c[num].p);
	swap (a[c[num].p], c[num].c);
	if (c[num].p >= l && c[num].p <= r) add (c[num].p);
}

signed main () {
	read (n), read (m);
	t = pow (n, 2.0 / 3);
	rep (i, 1, n) {
		read (a[i]);
		pos[i] = i / t + 1;
	}
	rep (i, 1, m) {
		char s[3];
		scanf ("%s", s);
		if (*s == 'Q') {
			++ ql;
			read (q[ql].l), read (q[ql].r);
			q[ql].k = ql;
			q[ql].cnt = cl;
		} else {
			++ cl;
			read (c[cl].p), read (c[cl].c);
		}
	}
	sort (q + 1, q + ql + 1, cmp);
	int l = 1, r = 0;
	rep (i, 1, ql) {
		while (l > q[i].l) add (-- l);
		while (l < q[i].l) sub (l ++);
		while (r > q[i].r) sub (r --);
		while (r < q[i].r) add (++ r);
		while (num < q[i].cnt) change (l, r, ++ num);
		while (num > q[i].cnt) change (l, r, num --);
		ans[q[i].k] = res;
	}
	rep (i, 1, ql) {
		printf ("%lld\n", ans[i]);
	}
	return 0;
}

3. 回滚莫队

普通莫队和带修莫队都是支持 \(O(1)\) 的插入和删除的,但有一些时候你无法将插入或删除做到 \(O(1)\)(比如下面的例题),这就会导致复杂度变高。于是这个时候就需要回滚莫队了。

经典例题 \(3\)

给出一个长度为 \(n\) 的正整数序列 \(A\),每次查询区间 \([l,r]\) 的数的最大值。

Solution

依旧当线段树和 ST 表做法被我卡了,如何用莫队解决?

容易发现,如果使用普通莫队,插入操作可以在 \(O(1)\) 的时间内完成,但是删除操作很难在 \(O(1)\) 时间完成,因为在你删除的时候你并不知道删去这个数后最大值是多少。那既然这样,我们干脆直接不做删除操作了,只做插入操作,不就能控制复杂度了吗?于是我们有了如下解决方案:

  1. 还是先分块,对询问的左端点所在块编号为第一关键字,右端点为第二关键字升序排序。(注意:这类问题用奇偶化排序会出错)
  2. 枚举每一块内的询问,设当前的块的右端点为 \(R\),当前处理到的区间的左右指针为 \(l\)\(r\),令 \(l\)\(r\) 初始为空区间 \([R+1,R]\)
  3. 处理在当前块中的每一个询问 \([ql,qr]\),如果当前询问的左右端点都在块内,就暴力计算;否则,右指针拓展到询问区间右端点 \(qr\),把结果存入 \(last\),然后把左指针 \(l\) 拓展到询问区间左端点 \(ql\) ,把结果存入答案 \(ans\)。最后再将 \(l\) 滚回 \(R+1\),并把结果滚回 \(last\)

那么代码就该像这样写:

bool cmp (MO x, MO y) {
	return pos[x.l] == pos[y.l] ? x.r < y.r : pos[x.l] < pos[y.l];
}

int calc (int l, int r) {
	int res = -inf;
	for (int i = l; i <= r; ++ i) {
		res = max (res, a[i]);
	}
	return res;
}

void add (int x) {
	res = max (res, a[x]);
}

int main () {
	read (n, m);
	int t = sqrt (n);
	for (int i = 1; i <= n; ++ i) read (a[i]), pos[i] = (i - 1) / t + 1;
	for (int i = 1; i <= m; ++ i) {
		read (q[i].l, q[i].r);
		q[i].id = i;
	}
	sort (q + 1, q + m + 1, cmp);
  //前面的没区别
	int x = 1;
	for (int i = 1; i <= pos[n]; ++ i) {//枚举询问所在的块
		res = lst = -inf;
		int R = min (i * t, n), l = R + 1, r = R;
		for (; pos[q[x].l] == i; ++ x) {
			if (pos[q[x].l] == pos[q[x].r]) {
				ans[q[x].id] = calc (q[x].l, q[x].r);
				continue;
			}//在同一块就暴力查
			while (r < q[x].r) add (++ r);
			lst = res;
			while (l > q[x].l) add (-- l);
			ans[q[x].id] = res;//查答案
			l = R + 1;
			res = lst;//回滚
		}
	}
	for (int i = 1; i <= m; ++ i) print (ans[i], '\n');//输出
	return 0;
}

然后发现这代码可以通过 ST 表的模板题,AC记录。。。

复杂度分析

设块长为 \(B\),则块数为 \(\frac{n}{B}\)

查询那里分两种情况:

  • 查询的左右端点在同一块,由于每一块的长度为 \(B\),那么暴力枚举的次数不超过 \(B\) 次,复杂度为 \(O(B)\)
  • 查询的左右端点不在同一块,移动指针复杂度显然是 \(O(n)\)

外面套了一层 \(O(\frac{n}{B})\) 的循环,于是总时间复杂度为 \(O(n+\frac{n^2}{B})\),取 \(B=\sqrt n\),则复杂度为 \(O(n+n\sqrt n)\)


\(4\)P14420 [JOISC 2014] 历史的研究 / Historical Research

作为研究 IOI 国历史的先驱者,乔伊教授获得了一本据称由古代 IOI 国居民所写的日记。乔伊教授计划通过分析这本日记来研究古代 IOI 国的生活,因此决定调查日记中记录的事件。

日记中记录了连续 \(N\) 天内每天发生的事件,每个事件被分类为若干种类型之一。第 \(i\) 天(\(1 \le i \le N\))发生的事件类型由整数 \(X_i\) 表示,\(X_i\) 的值越大,表示该事件的规模越大。

乔伊教授决定按以下方法分析日记:

  1. 从日记的 \(N\) 天中选择一个连续的若干天作为分析区间。
  2. 对于事件类型 \(t\),其“重要度”定义为 \(t \times\)(该区间内类型 \(t\) 的事件数量)。
  3. 对所有事件类型分别计算重要度,并求出其中的最大值。

你被乔伊教授委派编写一个用于分析的程序。该程序需在给定分析区间的情况下,能够求出重要度的最大值。

  • \(1 \le N \le 100000\)
  • \(1 \le Q \le 100000\)
  • \(1 \le X_i \le 1000000000\)\(1 \le i \le N\))。

Solution

线段树并不好做,考虑莫队。

注意到有求最大值的操作,那么就上回滚莫队。

\(cnt_x\)\(x\) 出现的次数,那么我们要求的答案就是 \(\max_{l\le i\le r}\{cnt_{a_i}\times a_i\}\)。依旧像经典例题 \(4\) 一样,对于查询区间左右端点在同一块的情况,我们直接暴力处理;否则就进行回滚莫队的正常操作:滚指针,再回滚。

注意要离散化。

Code
#include<bits/stdc++.h>
#define int long long
#define rep(i,a,b) for(register int i=(a);i<=(b);++i)
#define per(i,a,b) for(register int i=(a);i>=(b);--i)
#define pi pair<int,int>
#define mkp(a,b) make_pair((a),(b)) 
#define IOS cin.tie(0)->sync_with_stdio(0);
using namespace std;
const int N = 1e6 + 15, M = 1e3 + 15;

const int I_LOVE_CCF = 1;

int n, Q, t;
int a[N], b[N];
int pos[N];
int cnt[N], res, last, c[N];
int ans[N];
struct MO{
	int l, r;
	int k;
}q[N];

inline int read (int &n) {
	int x = 0, f = 1;
	char ch = getchar ();
	while (!isdigit (ch)) {
		if (ch == '-') f = -1;
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ 48);
		ch = getchar ();
	}
	n = x * f;
	return n;
}

bool cmp (MO x, MO y) {
	return pos[x.l] == pos[y.l] ? x.r < y.r : pos[x.l] < pos[y.l];
}

int calc (int l, int r) {
	int maxx = 0;
	rep (i, l, r) {
		c[a[i]] = 0;
	}
	rep (i, l, r) {
		c[a[i]] ++;
		maxx = max (maxx, c[a[i]] * b[a[i]]);
	}
	return maxx;
}

void add (int x) {
	++ cnt[a[x]];
	res = max (res, cnt[a[x]] * b[a[x]]);
}

signed main () {
	read (n), read (Q);
	t = sqrt (n);
	rep (i, 1, n) {
		b[i] = read (a[i]);
		pos[i] = (i - 1) / t + 1;
	}
	sort (b + 1, b + n + 1);
	int len = unique (b + 1, b + n + 1) - b - 1;
	rep (i, 1, n) {
		a[i] = lower_bound (b + 1, b + len + 1, a[i]) - b;
	}
	rep (i, 1, Q){
		read (q[i].l), read (q[i].r);
		q[i].k = i;
	}
	sort (q + 1, q + Q + 1, cmp);
	int x = 1;
	rep (i, 1, pos[n]) {
		res = last = 0;
		rep (j, 1, len) cnt[j] = 0;
		rep (j, 1, len) {
			int R = min (i * t, n), l = R + 1, r = R;
			for (; pos[q[x].l] == i; x ++) {
				if (pos[q[x].l] == pos[q[x].r]) {
					ans[q[x].k] = calc (q[x].l, q[x].r);
					continue;
				}
				while (r < q[x].r) add (++ r);
				last = res;
				while (l > q[x].l) add (-- l);
				ans[q[x].k] = res;
				while (l <= R) -- cnt[a[l ++]];
				res = last;
			}
		}
	}
	rep (i, 1, Q) {
		printf ("%lld\n", ans[i]);
	}
	return 0;
}

\(5\)【模板】回滚莫队&不删除莫队

给定一个序列,多次询问一段区间 \([l,r]\),求区间中相同的数的最远间隔距离

序列中两个元素的间隔距离指的是两个元素下标差的绝对值

对于 \(100\%\) 的数据,满足 \(1\leq n,m\leq 2\cdot 10^5\)\(1\leq a_i\leq 2\cdot 10^9\)

Solution

这玩意线段树根本维护不了,然后发现“最远”,于是考虑回滚莫队。

我们可以考虑记录下每一个数第一次出现的位置 \(st\) 和最后一次出现的位置 \(lst\),那么每一次我们只需要记录 \(\max\{lst-st\}\) 就行了。

然后就是像上面说的回滚莫队套路。然后就做完了。

注意每一轮查询的时候要把上一轮的清空,还要离散化。

Code
#include<bits/stdc++.h>
#define int long long
#define rep(i,a,b) for(register int i=(a);i<=(b);++i)
#define per(i,a,b) for(register int i=(a);i>=(b);--i)
#define pi pair<int,int>
#define mkp(a,b) make_pair((a),(b)) 
#define IOS cin.tie(0)->sync_with_stdio(0);
using namespace std;
const int N = 2e5 + 15, M = 1e3 + 15;

const int I_LOVE_CCF = 1;

int n, m, t;
int a[N], b[N];
int pos[N];
int lst[N], res, last, st[N], lt[N];
int c[N], idx;
int ans[N];
struct MO{
	int l, r;
	int k;
}q[N];

inline int read (int &n) {
	int x = 0, f = 1;
	char ch = getchar ();
	while (!isdigit (ch)) {
		if (ch == '-') f = -1;
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ 48);
		ch = getchar ();
	}
	n = x * f;
	return n;
}

bool cmp (MO x, MO y) {
	return pos[x.l] == pos[y.l] ? x.r < y.r : pos[x.l] < pos[y.l];
}

int calc (int l, int r) {
	int maxx = 0;
	rep (i, l, r) lt[a[i]] = 0;
	rep (i, l, r) {
		if (! lt[a[i]]) {
			lt[a[i]] = i;
		} else maxx = max (maxx, abs (i - lt[a[i]]));
	}
	return maxx;
}

signed main () {
	read (n);
	t = sqrt (n);
	rep (i, 1, n) read (a[i]), b[i] = a[i], pos[i] = (i - 1) / t + 1;
	sort (b + 1, b + n + 1);
	int len = unique (b + 1, b + n + 1) - b - 1;
	rep (i, 1, n) {
		a[i] = lower_bound (b + 1, b + len + 1, a[i]) - b;
	}
	read (m);
	rep (i, 1, m) {
		read (q[i].l), read (q[i].r);
		q[i].k = i;
	}
	sort (q + 1, q + m + 1, cmp);
	int x = 1;
	rep (i, 1, pos[n]) {
		res = last = 0;
		idx = 0;
		int R = min (i * t, n), l = R + 1, r = R;
		for (; pos[q[x].l] == i; x ++) {
			if (pos[q[x].l] == pos[q[x].r]) {
				ans[q[x].k] = calc (q[x].l, q[x].r);
				continue;
			}
			while (r < q[x].r) {
				++ r;
				lst[a[r]] = r;
				if (! st[a[r]]) st[a[r]] = r, c[++ idx] = a[r];
				res = max (res, abs (r - st[a[r]]));
			}
			last = res;
			while (l > q[x].l) {
				-- l;
				if (lst[a[l]]) res = max (res, abs (lst[a[l]] - l));
				else lst[a[l]] = l;	
			}
			ans[q[x].k] = res;
			while (l <= R) {
				if (lst[a[l]] == l) lst[a[l]] = 0;
				l ++;
			}
			res = last;
		}
		rep (k, 1, idx) lst[c[k]] = st[c[k]] = 0;
	}
	rep (i, 1, m) {
		printf ("%lld\n", ans[i]);
	}
	return 0;
}

补充:块长

我们上面所涉及到的题目的块长都是 \(\sqrt n\),但事实上这并不是最优的,最优的块长是 \(\frac{n}{\sqrt {Q}}\)。具体可以看下面:

不是很严谨的证明 设我们的块长为 $B$,那么经过上面的分析可以得到 $O(\frac{n^2}{B}+QB)$ 的总复杂度。设 $t=\frac{n^2}{B}+QB$,变形可得

\[QB^2-tB+n^2=0 \]

发现这是一个二次函数。化为顶点式:

\[Q(B-\frac{t}{2Q})^2-\frac{t^2}{4Q}+n^2=0 \]

\(t\) 当成常量,显然当 \(B=\frac{t}{2Q}\) 时函数有最小值。将 \(t\) 代回去:

\[B=\frac{\frac{n^2}{B}+QB}{2Q} \]

等号两边同时乘以 \(2Q\)

\[2QB=\frac{n^2}{B}+QB \]

化简:

\[QB^2-n^2=0 \]

解得合法的 \(B=\frac{n}{\sqrt {Q}}\),即最优块长为 \(\frac{n}{\sqrt Q}\)。。

为什么说不严谨呢,是因为 \(t\) 其实也是一个关于 \(B\) 的函数,直接当成常量算不是很对(虽然能推出结果)。

严谨的证明 一样的,设我们的块长为 $B$,那么总复杂度是 $O(\frac{n^2}{B}+QB)$。显然可以得到 $\frac{n^2}{B}>0,QB>0$。

\(a=\frac{n^2}{B},b=QB\),根据均值不等式得到 \(a+b\ge2\sqrt{ab}\)。当 \(a=b\) 的时候有最小值(即取等号),所以当 \(\frac{n^2}{B}=QB\) 时复杂度最小。解方程得到 \(B=\frac{n}{\sqrt Q}\),即最优块长为 \(\frac{n}{\sqrt Q}\)

不过用这个块长可以被【数据删除】的人构造数据卡成 RE,所以说看情况吧,毕竟也很少有人【数据删除】成那样。

莫队二次离线

还没学,先咕着。

结束语

到最后你会发现,莫队这个东西本质上就是一个暴力,但是它可以通过纯暴力无法通过的题,看着就像一个绅士干莽夫的事情,却十分优雅。因此莫队也被称为是“优雅的暴力”。


习题

普通莫队

[ABC293G] Triple Index

P4462 [CQOI2018] 异或序列

带修莫队

P2464 [SDOI2008] 郁闷的小 J

CF940F Machine Learning

P4074 [WC2013] 糖果公园

回滚莫队

CF620F Xors on Segments

posted @ 2025-12-23 17:45  XXh_Laoxu  阅读(2)  评论(0)    收藏  举报

转载请注明出处!


#页面摧毁游戏#
使用【上下左右】控制飞行器的运动
使用【空格】发射导弹
点击开始摧毁