莫队(普通操作)C++

由于&R闲着无聊,今天上午学习的莫队也并不是完全掌握,老师才发的PPT也没有保存,于是决定乱写一篇靠着自己的理解写一篇关于莫队用法的博客......如果有错的麻烦之处,纯属&R自己理解有问题哈。

一.莫队的含义

莫队是在一篇国家集训队的论文中被提出的。本文只讨论普通的莫队用法。

莫队其实就是一种比较优雅的暴力算法,它只能支持离线计算,时间复杂度为O(m+n)sqrt(n)

其中操作的具体方法是,将需要操作的数列,按一定规则排序,规定左端点和右端点,然后将每次计算数列值的时候将左端点和右端点移到目标点上,边移动遍计算。这样的好处是可以避免一些重复计算,这样说可能有亿点点抽象,我们还是先来看一下一些关于莫队的具体操作吧。如果看不懂,我们可以先看第三大点的例题,结合起来看。是复习的话当我没说

二.莫队的基本操作

由于写作水品不加,于是决定先甩一堆模板

1.添加点

void add(int i)
{
	if(cnt[a[i]] == 0)	cur ++ ;
	cnt[a[i]] ++ ;
}

2.删除点

void del(int i)
{
	cnt[a[i]] -- ;
	if(cnt[a[i]] == 0)	cur -- ;	
}

3.bool排序

(1)未优化

bool cmp(Node a, Node b)
{
	int Num = sqrt(n) ;
	return (a.l/Num != b.l/Num)?a.l<b.l:a.r<b.r ;
}

(2)奇偶优化

bool cmp(Node a, Node b)
{
	int Num = sqrt(n) ;
	return (a.l/Num != b.l/Num)?a.l<b.l:((a.l/Num)&1)?a.r<b.r:a.r>b.r ;
}

备注:!=可以改成^(这样更快),&的等于% 2 == 1。

4.移点的具体操作

void Moteam()
{
	for(int i = 1; i <= m; i ++){
		while(l < P[i].l) del(l ++) ;
		while(r < P[i].r) add(++ r) ;
		while(l > P[i].l) add(-- l) ;
		while(r > P[i].r) del(r --) ;
		ans[P[i].id] = cur ;
	}
}

备注:因为是离线操作,所以拍完序后顺序会被打乱,我没输出时必须还是要按原来的顺序输出,所以需要保存每个点原先进来的id

三.例题

茫然的我于是决定写几道例题来看看,感觉是自己写给自己理解的QAQ

1.HH的项链

(1)题目描述

HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。

有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答;因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

(2)输入格式

第一行:一个整数N,表示项链的长度。

第二行:N 个整数,表示依次表示项链中贝壳的编号(编号为0 到1000000 之间的整数)。

第三行:一个整数M,表示HH 询问的个数。

接下来M 行:每行两个整数,L 和R(1 ≤ L ≤ R ≤ N),表示询问的区间。

(3)输出格式

M 行,每行一个整数,依次表示询问对应的答案。

(4)样例

样例输入

6
1 2 3 4 3 5
3
1 2
3 5
2 6

样例输出

2
2
4

(5)数据范围与提示

对于20%的数据,N ≤ 100,M ≤ 1000;
对于40%的数据,N ≤ 3000,M ≤ 200000;
对于100%的数据,N ≤ 50000,M ≤ 200000;

(6)题目分析

没什么好分析的不就是板子题吗?

        我们想想(用莫队想哈,虽然树状数组也能做Q^Q),如果我们用正常的超级暴力的想法来做的话,我们就需要先枚举m,然后从左端点开始枚举,枚举到右端点,用一个数组来判断这个数是否是第一次出现,如果是的话,我们就将ans累加,最后输出,清零,进行m操作即可,时间复杂度为O(mn)。

        于是我们决定将这个暴力打的优雅一点点。

        我们先思考,假设我们计算了1到4的,然后我们又要计算2到5,那么2到4这个部分就会重复计算,还不如令l = 1, r = 4,用cnt[i]来存储i出现的次数,用cur来表示目前一共出现了多少个没有重复的数。那么l向右移动后,”原先那个数“的个数cnt[l(这里的l是还没移动的坐标)]就会减一,如果个数变成零,那么cur随之减少1。r向右移动,如果cnt[Num[r]]==0,那么就说明这个数是第一次出现,cur++。

        一共有四种情况,详细见二.1.2.4.

        那么这样真的能优化吗?如果左右两个点总是跨度很大,那么我们不如暴力

        智慧的前辈们已经替我们解决了这个问题。

        我们可以将这一条线分成很多组,经过平衡,分成根号(n)组是最合适的。所以我们尽量控制左端点在根号n的范围内移动,右端点在n的范围内移动,所以就有了二.3.(1)的排序

        可是我们还有一种更优的排序方法二.3.(2)

        我们思考当第一组(共根号n组)的r移动到最右边时(r是按从左到右排了序的),我们在计算第二组的时候为了让r少跑一点,于是将r从右到左排序,于是就有了奇数组按a.r < b.r, 偶数组按a.r > b.r的排序方法

        

#include <bits/stdc++.h>
using namespace std ;
const int MAXM = 200005 ;
int n, a[50005], m, cur = 0, cnt[1000005], Num = 1, l = 1, r = 0, ans[MAXM] ;
struct Node
{
	int l, r, id ;
}P[MAXM] ;
bool cmp(Node a, Node b){
	return (a.l / Num) != (b.l / Num) ? a.l < b.l : ((a.l / Num) & 1) ? a.r < b.r : a.r > b.r ;
}
int read()
{
	int x = 0, f = 1 ;
	char c = getchar() ;
	while(c < '0' || c > '9'){
		if(c == '-')	f = -1 ;
		c = getchar() ;
	}
	while(c >= '0' && c <= '9'){
		x = x * 10 + c - '0' ;
		c = getchar() ;
	}
	return x * f ;
}
void add(int i)
{
	if(cnt[a[i]] == 0)	cur ++ ;
	cnt[a[i]] ++ ; 
}
void det(int i)
{
	cnt[a[i]] -- ;
	if(cnt[a[i]] == 0)	cur -- ;
}
void Moteam()
{
	for(int i = 1; i <= m; i ++){
		while(l < P[i].l){
			det(l ++) ;
		}
		while(r < P[i].r){
			add(++ r) ;
		}
		while(l > P[i].l){
			add(-- l) ;
		}
		while(r > P[i].r){
			det(r --) ;
		}
		ans[P[i].id] = cur ;
	}
}
void Init()
{
	n = read() ;
	memset(cnt, 0, sizeof(cnt)) ;
	Num = (int)sqrt(n) ;
	for(int i = 1; i <= n; i ++){
		a[i] = read() ;
	}
	m = read() ;
	for(int i = 1; i <= m; i ++){
		P[i].l = read(), P[i].r = read(), P[i].id = i ;
	}
	sort(P + 1, P + m + 1, cmp) ;
}
int main()
{
	Init() ;
	Moteam() ;
	for(int i = 1; i <= m; i ++)
	{
		printf("%d\n", ans[i]) ;
	}
}

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值