莫队算法学习笔记

莫队算法学习笔记

算法简介

莫队算法是一种离线算法, 用于解决一些区间问题, 过程中会与分块算法结合使用

解决问题

  1. 区间众数

  2. 区间相等数对的个数

例题讲解

\(洛谷\rm{P}1494[国家集训队] 小 \rm{Z} 的袜子\)

题目传送门: P1494 [国家集训队] 小 Z 的袜子

P1494 [国家集训队] 小 Z 的袜子

题目描述

作为一个生活散漫的人,小 Z 每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿。终于有一天,小 Z 再也无法忍受这恼人的找袜子过程,于是他决定听天由命……

具体来说,小 Z 把这 \(N\) 只袜子从 \(1\)\(N\) 编号,然后从编号 \(L\)\(R\) 的袜子中随机选出两只来穿。尽管小 Z 并不在意两只袜子是不是完整的一双,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。

你的任务便是告诉小 Z,他有多大的概率抽到两只颜色相同的袜子。当然,小 Z 希望这个概率尽量高,所以他可能会询问多个 \((L,R)\) 以方便自己选择。

然而数据中有 \(L=R\) 的情况,请特判这种情况,输出0/1

输入格式

输入文件第一行包含两个正整数 \(N\)\(M\)\(N\) 为袜子的数量,\(M\) 为小 Z 所提的询问的数量。接下来一行包含 \(N\) 个正整数 \(C_i\),其中 \(C_i\) 表示第 \(i\) 只袜子的颜色,相同的颜色用相同的数字表示。再接下来 \(M\) 行,每行两个正整数 \(L, R\) 表示一个询问。

输出格式

包含 \(M\) 行,对于每个询问在一行中输出分数 \(A/B\) 表示从该询问的区间 \([L,R]\) 中随机抽出两只袜子颜色相同的概率。若该概率为 \(0\) 则输出 0/1,否则输出的 \(A/B\) 必须为最简分数。(详见样例)

输入输出样例 #1

输入 #1
6 4
1 2 3 3 3 2
2 6
1 3
3 5
1 6
输出 #1
2/5
0/1
1/1
4/15

说明/提示

\(30\%\) 的数据中,\(N,M\leq 5000\)

\(60\%\) 的数据中,\(N,M \leq 25000\)

\(100\%\) 的数据中,\(N,M \leq 50000\)\(1 \leq L \leq R \leq N\)\(C_i \leq N\)

解题思路

这道题是莫队算法的模板题。

第一步: 题目简化(分析)

首先遇到这种有背景且题目描述冗长的题目需要先简化题目, 概括出题目大意, 方便我们之后进行弱化与求解。

然后在一个长度为\(x\)的区间中总共有\(frac{x(x-1)}{2}\)对二元组, 那么概率就是\(\frac{分子}{x(x-1)/2}\)

其中分子就是区间内颜色相同的袜子的对数, 那么我们下面的问题就主要是求分子的过程

其实将分子简化就是:

求区间内有多少对二元组\((i,j)\)满足\(a_i=a_j\)

第二步: 题目分析

其次, 我们需要先对题目进行分析, 看看有没有我们之前的知识点是可以解决的。

看到这道题, 有关区间的首先想到\(\rm{CQB}\)系列问题, 所以想到用线段数或者树状数组来解决, 但是这道题并不具有线段树能解决的问题的性质, 因为无法进行两个子树信息的合并, 所以这道题不能用线段树来解决。

所以就需要一个新的算法来解决这道题, 那就是莫队算法。

第三步: 题目弱化

然后, 在提高组阶段的难题都是要经过一个弱化的过程, 先把题目弱化到一个可以解决的程度, 然后再一步一步地优化和推广到原问题。

那么这道题就可以先弱化到:假设每次询问的区间[l,r]中l始终等于1

先解决这个弱化版的问题:

我们可以考虑先将询问按区间右端点从小到大排序, 但排序前要记录一下这个区间的编号(这一部分就属于离线算法的操作了)。然后接下来我们按顺序处理:

假设我们已经有了第一个区间的答案了

那么计算下一个区间的答案时, 我们只需要从第一个区间的右端点\(r_1\)到下一个区间的右端点\(r_2\)暴力遍历每一个点, 将这个点的贡献加入答案中, 相当于将\(r_1\)\(r_2\)的每一个点都插入到第一个区间中就得到了第二个区间。

接下来的问题就是怎样高效(时间复杂度低)计算其中每个点对答案的贡献

假设第一个区间的答案为\(ans\), 在第一个区间后插入一个\(k\), 这个\(k\)在第一个区间中出现了\(cnt\)次, 那么这个\(k\)对答案的贡献就是\(cnt\), 然后将\(k\)加入到区间中\(cnt++\)

那么什么数据结构支持高效查询一个数在区间中出现的次数呢?答案是桶

那么我们简化版的题目就解决了

简化版的算法流程:

  1. 将询问按区间右端点从小到大排序, 并记录询问的编号

  2. 从左到右遍历每个询问, 对于每个询问:

从上一个询问的右端点\(r_1\)到当前询问的右端点\(r_2\)暴力遍历每一个点, 将这个点的在上一个询问区间的出现加入答案中, 然后他的出现次数加一。每个点的出现次数用桶维护

之后我们考虑怎么将这个算法推广到原问题

第四步: 题目推广

推广到原问题, 不过是区间的左端点不再是\(1\)了, 所以只需要在排序上修改一下, 然后在暴力遍历的时候也要加上\(l\)的影响, 就可以了。还有一些细节要注意一下就好了, 详见代码注释

其中排序过程中左端点是用了分块的思想, 以左端点所在的块从小到大排序, 这是用了双关键字的排序。

时间复杂度分析

首先计算块的大小, 使时间复杂度最小。

假设块的大小为\(B\), 则有\(\frac{n}{B}\)个块, 那么时间复杂度为:
\(\Theta(mB+ \frac{n}{B} \times n )\)

那么我们通过均值不等式可以得到当$mB=\frac{n}{B} \times n \(是时间复杂度最小, 所以\)B= \frac{n}{\sqrt{m}}$

\(\rm{AC}\) \(\rm{Code}\)

#include <bits/stdc++.h>

using namespace std;

#define int long long

const int maxn = 5e4 + 10;

int siz; // 块的大小
int n, m, a[maxn]; // n个数, m个询问, k种颜色, a[i]表示第i个数的颜色
// res表示当前区间的答案, ans[i]表示第i个询问的答案, cnt[i]表示颜色i出现的次数, num[i]表示第i个区间的二元组数
long long res, ans[maxn], cnt[maxn], num[maxn];

struct Query{ // 离线存询问的结构体
	int l, r, t; // l, r表示询问的区间, t表示询问的编号
	bool operator<(const Query &tmp)const{ // 重载排序
		int pie1 = l / siz; // 当前询问左端点所在块
		int pie2 = tmp.l / siz; // 所比较的询问左端点所在块
		if(pie1 == pie2) return r < tmp.r; // 如果在同一块, 就按右端点排序
		else return pie1 < pie2; // 否则按左端点的块排序
	}
}q[maxn]; // 询问

// 莫队算法删除操作
void erase(int x){
	// 删除x并且更新答案
	res -= cnt[x] * (cnt[x] - 1) / 2; //先减去原来的, 这里算的就是二元组个数
	cnt[x] -- ; // 然后更新cnt
	res += cnt[x] * (cnt[x] - 1) / 2; // 再计算更新后的
}

void insert(int x){
	// 加入x并且更新答案, 与删除操作同理
	res -= cnt[x] * (cnt[x] - 1) / 2;
	cnt[x] ++ ;
	res += cnt[x] * (cnt[x] - 1) / 2;
}

signed main(){
	// 输入
	scanf("%lld%lld", &n, &m);
	for(int i = 1; i <= n; i ++ ){
		scanf("%lld", &a[i]);
	}
	// 离线存储询问
	for(int i = 1; i <= m; i ++ ){
		scanf("%lld%lld", &q[i].l, &q[i].r);
		q[i].t = i;
		num[i] = (q[i].r - q[i].l + 1) * (q[i].r - q[i].l) / 2; // 计算二元组个数
	}

	siz = n / (sqrt(m) + 1); // 计算块的大小
	sort(q + 1, q + 1 + m); // 排序
	
	int l = 1, r = 0; // 利用双指针维护区间, 开始时区间为空
	for(int i = 1; i <= m; i ++ ){
		// 这里注意一个细节, 先进行insert的操作, 在进行erase的操作, 防止减成负数
		while(l > q[i].l){ // 当前一个询问的左端点在当前区间的左边时, 加入两个左端点之间的数
			l -- ; // 注意先减再插入
			insert(a[l]);
		}
		while(r < q[i].r){ // 当前一个询问的右端点在当前区间的右边时, 加入两个右端点之间的数
			r ++ ; // 注意先加再插入
			insert(a[r]);
		}
		while(l < q[i].l){ // 当前一个询问的左端点在当前区间的右边时, 删除两个左端点之间的数
			erase(a[l]); // 注意先删除再加
			l ++ ;
		}
		while(r > q[i].r){ // 当前一个询问的右端点在当前区间的左边时, 删除两个右端点之间的数
			erase(a[r]); // 注意先删除再加
			r -- ;
		}
		ans[q[i].t] = res; // 当前询问的答案就是现在的res
	}
	
	// 输出
	for(int i = 1; i <= m; i ++ ){
		if(ans[i] ==0 || num[i] == 0){ // 如果答案是0则输出0/1(题目要求的)
			printf("0/1\n");
			continue;
		}
		int gcd = __gcd(num[i], ans[i]);
		printf("%lld/%lld\n", ans[i] / gcd, num[i] / gcd); // 化成既约分数
	}
	return ^_^;
}

\(洛谷\) \(\rm{P}2709\) \(小\rm{B}的询问\)

P2709 小B的询问

题目描述

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

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

其中 \(c_i\) 表示数字 \(i\)\([l,r]\) 中的出现次数。
小B请你帮助他回答询问。

输入格式

第一行三个整数 \(n,m,k\)

第二行 \(n\) 个整数,表示 小B 的序列。

接下来的 \(m\) 行,每行两个整数 \(l,r\)

输出格式

输出 \(m\) 行,每行一个整数,对应一个询问的答案。

输入输出样例 #1

输入 #1
6 4 3
1 3 2 1 1 3
1 4
2 6
3 5
5 6
输出 #1
6
9
5
2

说明/提示

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

解题思路

与上道题同理, 只不过加了一个平方, 详见代码注释

\(\rm{AC}\) \(\rm{Code}\)

#include <bits/stdc++.h>

using namespace std;

const int maxn = 5e4 + 10;

int siz; // 块的大小
int n, m, k, a[maxn]; // n个数, m个询问, k种颜色, a[i]表示第i个数的颜色
// res表示当前区间的答案, ans[i]表示第i个询问的答案, cnt[i]表示颜色i出现的次数
long long res, ans[maxn], cnt[maxn];

struct Query{ // 离线存询问的结构体
	int l, r, t; // l, r表示询问的区间, t表示询问的编号
	bool operator<(const Query &tmp)const{ // 重载排序
		int pie1 = l / siz; // 当前询问左端点所在块
		int pie2 = tmp.l / siz; // 所比较的询问左端点所在块
		if(pie1 == pie2) return r < tmp.r; // 如果在同一块, 就按右端点排序
		else return pie1 < pie2; // 否则按左端点的块排序
	}
}q[maxn]; // 询问

// 莫队算法删除操作
void erase(int x){
	// 删除x并且更新答案
	res -= cnt[x] * cnt[x]; // 按照题目所说的平方, 先减去原来的
	cnt[x] -- ; // 然后更新cnt
	res += cnt[x] * cnt[x]; // 再计算更新后的
}

void insert(int x){
	// 加入x并且更新答案, 与删除操作同理
	res -= cnt[x] * cnt[x];
	cnt[x] ++ ;
	res += cnt[x] * cnt[x];
}

int main(){
	// 输入
	scanf("%d%d%d", &n, &m, &k);
	for(int i = 1; i <= n; i ++ ){
		scanf("%d", &a[i]);
	}
	// 离线存储询问
	for(int i = 1; i <= m; i ++ ){
		scanf("%d%d", &q[i].l, &q[i].r);
		q[i].t = i;
	}
	
	siz = n / (sqrt(m) + 1); // 计算块的大小
	sort(q + 1, q + 1 + m); // 排序
	
	int l = 1, r = 0; // 利用双指针维护区间, 开始时区间为空
	for(int i = 1; i <= m; i ++ ){
		// 这里注意一个细节, 先进行insert的操作, 在进行erase的操作, 防止减成负数
		while(l > q[i].l){ // 当前一个询问的左端点在当前区间的左边时, 加入两个左端点之间的数
			l -- ; // 注意先减再插入
			insert(a[l]);
		}
		while(r < q[i].r){ // 当前一个询问的右端点在当前区间的右边时, 加入两个右端点之间的数
			r ++ ; // 注意先加再插入
			insert(a[r]);
		}
		while(l < q[i].l){ // 当前一个询问的左端点在当前区间的右边时, 删除两个左端点之间的数
			erase(a[l]); // 注意先删除再加
			l ++ ;
		}
		while(r > q[i].r){ // 当前一个询问的右端点在当前区间的左边时, 删除两个右端点之间的数
			erase(a[r]); // 注意先删除再加
			r -- ;
		}
		ans[q[i].t] = res; // 当前询问的答案就是现在的res
	}
	
	// 输出
	for(int i = 1; i <= m; i ++ ){
		printf("%lld\n", ans[i]);
	}
	return ^_^;
}

\(洛谷\) \(\rm{P}4462\) \([CQOI2018] 异或序列\)

题目描述

已知一个长度为 \(n\) 的整数数列 \(a_1,a_2,\dots,a_n\),给定查询参数 \(l,r\),问在 \(a_l,a_{l+1},\dots,a_r\) 区间内,有多少子区间满足异或和等于 \(k\)。也就是说,对于所有的 \(x,y (l \leq x \leq y \leq r)\),能够满足 \(a_x \oplus a_{x+1} \oplus \dots \oplus a_y = k\)\(x,y\) 有多少组。

输入格式

输入文件第一行,为 \(3\) 个整数 \(n,m,k\)

第二行为空格分开的 \(n\) 个整数,即 \(a_1,a_2,..a_n\)

接下来 \(m\) 行,每行两个整数 \(l_j,r_j\),表示一次查询。

输出格式

输出文件共 \(m\) 行,对应每个查询的计算结果。

输入输出样例 #1

输入 #1
4 5 1
1 2 3 1
1 4
1 3
2 3
2 4
4 4
输出 #1
4
2
1
2
1

说明/提示

对于 \(30\%\) 的数据,\(1 \leq n, m \leq 1000\)

对于 \(100\%\) 的数据,\(1 \leq n, m \leq 10^5\)\(0 \leq k, a_i \leq 10^5\)\(1 \leq l_j \leq r_j \leq n\)

解题思路

首先, 异或有几个性质:

  1. 交换律: \(a \oplus b = b \oplus a\)

  2. 结合律: \((a \oplus b) \oplus c = a \oplus (b \oplus c)\)

  3. 自反性: \(a \oplus b \oplus b = 0\)

  4. 恒等性: \(a \oplus 0 = a\)

  5. 归零性: \(a \oplus a = 0\)

由以上三个性质可以得出:

  1. 异或和加法一样也有前缀异或和后缀异或和\((pre_i=\oplus_{j=1}^{i}a[i])\)

  2. 异或也与加法一样有可差分性, 如:\(\oplus_{i=l}^{r}a[i]=pre_r\oplus pre_{l-1}\)

那么知道以上的性质后, 尝试来解决这道题。

首先, 在考场上我们需要判断出这道题考察了哪些知识点, 再往里面套, 当我们题做多后就能看出来了, 所以假设我们知道这是一道莫队的题目, 那我们怎么往莫队里套呢?

看到题目, 要求满足\(a_x \oplus a_{x+1} \oplus \dots \oplus a_y = k\)\(x,y\) 有多少组, 那么根据上面的可差分性可以知道:

\(a_x \oplus a_{x+1} \oplus \dots \oplus a_y = pre_r \oplus pre_{l-1}\)

然后我们就需要在\(pre\)也就是前缀异或和中处理问题, 现在开始弱化问题:假设k=0那么该如何求解这个弱化的问题?

首先将\(k=0\)代入上式:

\(pre_r \oplus pre_{l-1}=0\)

根据异或的归零性: 当且仅当\(pre_r=pre_{l-1}\)时上式成立

这不就是莫队算法在求区间内有多少个\(pre_i=pre_j\)的二元组吗?

但要注意一点是求区间\([l-1,r]\)的二元组的。

那么推广到原问题, 如果\(k \neq 0\)呢?

还是将\(k\)代入上式:

\(pre_r \oplus pre_{l-1}=k\)

根据异或的交换律和结合律: \(pre_r=pre_{l-1} \oplus k\)

那么其实也就是求区间内有多少个\(pre_i=pre_j \oplus k\)的二元组, 同样也可以用莫队来解决。

还有一些代码细节详见代码注释。

\(\rm{AC}\) \(\rm{Code}\)

#include <bits/stdc++.h>

using namespace std;

const int maxn = 1e5 + 10;

#define int long long

// 含义见题面
int n, m, k;
int a[maxn];

// siz是块的大小, res动态存储答案, ans[i]是第i个询问的答案
// pre是前缀异或和, cnt是出现次数
int siz, res, ans[maxn];
int pre[maxn], cnt[2 * maxn];

// 离线存储询问的结构体
struct Query{
	int l, r, t; // l, r是左右端点, t是第几个询问(有点像离散化)
	bool operator<(const Query &tmp)const{ // 莫队的排序
		// 先按照左端点所在的块从小到大排序, 再按照右端点从小到大排序
		int pie1 = l / siz;
		int pie2 = tmp.l / siz;
		if(pie1 == pie2) return r < tmp.r;
		else return pie1 < pie2;
	}
}q[maxn];

// 莫队的插入函数
void insert(int x){
    res += cnt[x ^ k]; // 先统计cnt[x ^ k]的个数
    cnt[x] ++ ; // cnt[x]的出现个数+1
}
// 莫队的删除函数
void erase(int x){
    cnt[x] -- ; // 先更新cnt[x]
    res -= cnt[x ^ k]; // 再删除cnt[x ^ k]所带来的影响 
}

signed main(){
	// 输入
	scanf("%lld%lld%lld", &n, &m, &k);
	for(int i = 1; i <= n; i ++ ){
		scanf("%lld", &a[i]);
		pre[i] = pre[i - 1] ^ a[i]; // 计算前缀异或和
	}
	// 读入询问
	for(int i = 1; i <= m; i ++ ){
		scanf("%lld%lld", &q[i].l, &q[i].r);
		q[i].t = i;
	}

	res = 0; // 初始化答案
	siz = n / (sqrt(m) + 1); // 计算块的大小
	sort(q + 1, q + 1 + m); // 排序
	memset(cnt, 0, sizeof cnt); // 初始化cnt数组

	cnt[0] = 1; // 由于是区间[l - 1, r]的, 所以l-1可能为0, pre[0] = 0, 所以cnt[0]=1
	int l = 0, r = 0; // 因为是统计[l - 1, r]的, 所以这里的l从0开始
	for(int i = 1; i <= m; i ++ ){
		// 唯一要注意的一点: 计算区间[l - 1, r]的答案
		// 其他的和普通莫队一样
		while(l > (q[i].l - 1)) insert(pre[--l]);
		while(r < q[i].r) insert(pre[++r]);
		while(l < (q[i].l - 1)) erase(pre[l++]);
		while(r > q[i].r) erase(pre[r--]);
		ans[q[i].t] = res;
	}
	
	// 输出
	for(int i = 1; i <= m; i ++ ){
		printf("%lld\n", ans[i]);
	}
	return ^_^;
}
posted @ 2025-04-05 14:44  a_noooooob  阅读(23)  评论(0)    收藏  举报
———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/LogicYarn/article/details/141198668