学习莫队算法 (概念 + 时间复杂度分析 + 适用范围 + 例题:小B的询问)

看了很多博客,终于看的差不多了

勉强算是一个算法复习博客吧

  • 问题引入
  • 介绍莫队算法及其实现过程
  • 时间复杂度
  • 莫队算法适用范围
  • 例题:小B的询问
    • 题目
    • 简单题解
    • 代码实现

问题引入

给定一个大小为N的数组,数组中所有元素的大小<=N。你需要回答M个查询。
每个查询的形式是L,R。你需要回答在范围[ L,R ]中至少重复2次的数字的个数


如果按照以往的想法,就会是 O ( n 2 ) O(n^2) O(n2)的暴力枚举,

for ( int i = 1;i <= Q;i ++ ) {
		scanf ( "%d %d", &l, &r );
		for ( int j = l;j <= r;j ++ ) {
			count[a[j]] ++;
			if ( count[a[j]] == 3 )
				result ++;
		}
	}

就算加一些优化,用l,r采取指针转移,但总归上还是在 [ 1 , n ] [1,n] [1,n]区间内进行移动
最坏多半也是逼近于 O ( n 2 ) O(n^2) O(n2)

void add ( int x ) {
	count[a[x]] ++;
	if ( count[a[x]] == 3 )
		result ++;
}
void removed ( int x ) {
	count[a[x]] --;
	if ( count[a[x]] == 2 )
		result --;
}
for ( int i = 1;i <= m;i ++ ) {
	scanf ( "%d %d", &l, &r );
	while ( curl < l )
		removed ( curl ++ );
	while ( curl > l )
		add ( -- curl );
	while ( curr > r )
		removed ( curr -- );
	while ( curr < r )
		add ( ++ curr );
	printf ( "%d\n", result );
}

add:添加该位置的元素到当前集合内,并且更新答案
remove :从当前集合内删除该位置的元素,并更新答案


那么这个时候莫队算法就重磅登场了
学习莫队算法 (概念 + 时间复杂度分析 + 适用范围 + 例题:小B的询问)_第1张图片
为什么叫做莫队算法了呢?
据说算法是由之前的国家队队长莫涛发明的,他的队友平日里称他为莫队,所以称之为莫队算法


介绍莫队算法及其实现过程

莫队算法就是一个离线算法,仅仅调整了处理查询的顺序
实现过程如下↓:
将给定的输入数组分为 √ n √n n块。每一块的大小为 n / √ n n/√n n/n
每个L落入其中的一块,每个R也落入其中的一块
如果某查询的L落在第i块中,则该查询属于第i块

所有的询问首先按照所在块的编号升序排列(所在块的编号是指询问的L属于的块
如果编号相同,则按R值升序排列

莫队算法将依次处理第1块中的查询,然后处理第2块,最后直到第 n − √ n n-√n nn
可以有很多的查询属于同一块。

例如:假设我们有3个大小为3的块(0-2,3-5,6-8):
{0, 3} {1, 7} {2, 8} {7, 8} {4, 8} {4, 4} {1, 2}
先根据所在块的编号重新排列它们
第1块:{0, 3} {1, 7} {2, 8} {1, 2}
第2块:{4, 8} {4, 4}
第3块:{7, 8}
接下来按照R的值重新排列
第一块:{1, 2} {0, 3} {1, 7} {2, 8}
第二块:{4, 4} {4, 8}
第三块: {7, 8}
上述过程是正确,因为我们只是重新排列了查询的顺序
学习莫队算法 (概念 + 时间复杂度分析 + 适用范围 + 例题:小B的询问)_第2张图片


时间复杂度

我们说了这么多,选用莫队算法无非就是想把时间复杂度给降下来
接下来我们来看看真正莫队的时间复杂度是多少,其实我看了很多博客也是有点懵逼
学习莫队算法 (概念 + 时间复杂度分析 + 适用范围 + 例题:小B的询问)_第3张图片

上面的代码就是起一个铺垫作用,
所有查询的复杂性是由4个while循环决定的
前2个while循环可以理解为左指针(curl)的移动总量,
后2个 while循环可以理解为右指针(curr)的移动总量
这两者的和将是总复杂性


先算右指针
对于每个块,查询是递增的顺序排序,所以右指针(curr)按照递增的顺序移动
在下一个块的开始时,指针可能在最右端,将移动到下一个块中的最小的R处
又可以从本块最左端移动到最右端
这意味着对于一个给定的块,右指针移动的量是 O ( n ) O(n) O(n)(curr可以从1跑到最后的n)
我们有 O ( √ n ) O(√n) O(n)块,所以总共是 O ( n ∗ √ n ) O(n*√n) O(nn)


接下来看看左指针怎样移动
对于每个块,所有查询的左指针落在同一个块中,当我们从一个查询移动到另个一查询左指针会移动,
但由于前一个L与当前的L在同一块中,此移动是 O ( √ n ) O(√n) O(n)(块的大小)
在每一块中左指针的移动总量是 O ( Q ∗ √ n ) O(Q∗√n) O(Qn),(Q是落在那个块的查询的数量)
对于所有的块,总的复杂度为 O ( m ∗ √ n ) O(m∗√n) O(mn)


综上,总复杂度为 O ( ( n + m ) ∗ √ n ) = O ( n ∗ √ n ) O((n+m)∗√n)=O(n∗√n) O((n+m)n)=O(nn)


学习莫队算法 (概念 + 时间复杂度分析 + 适用范围 + 例题:小B的询问)_第4张图片实在无法理解就跳过吧
(如果有通俗易懂的解释欢迎评论)

莫队算法适用范围

首先莫队算法是一个离线算法,所以如果问题是在线操作带修或者强制特殊的顺序
莫队就失去了它的效应
学习莫队算法 (概念 + 时间复杂度分析 + 适用范围 + 例题:小B的询问)_第5张图片


其次一个重要的限制性:add和remove的操作
当有些题目的add和remove耗时很大, O ( √ N ) O(√N) O(N)时就应该思考能否卡过


但是还是有很大一部分区间查询的题可以由莫队进行完成
所以高度总结概括:只有查询先想莫队,查询待修找线段树
学习莫队算法 (概念 + 时间复杂度分析 + 适用范围 + 例题:小B的询问)_第6张图片


接下来让我们来一道小练习,去体会莫队的神奇在这里插入图片描述

例题:小B的询问

题目

小B有一个序列,包含N个1~K之间的整数。他一共有M个询问,
每个询问给定一个区间[L…R],求Sigma(c(i)^2)的值,
其中i的值从1到K,其中c(i)表示数字i在[L…R]中的重复次数。
小B请你帮助他回答询问。

输入格式
第一行,三个整数N、M、K。
第二行,N个整数,表示小B的序列。
接下来的M行,每行两个整数L、R。
输出格式
M行,每行一个整数,其中第i行的整数表示第i个询问的答案。

输入输出样例
输入
6 4 3
1 3 2 1 1 3
1 4
2 6
3 5
5 6
输出
6
9
5
2
说明/提示
对于全部的数据,1<=N、M、K<=50000

简单题解

说了是算法模板入门题,肯定不会把你拒之门外,还是要让你摸摸门的
这个题就是要简单处理一下 ∑ ( c ( i ) 2 ) ∑(c(i)^2) (c(i)2),当 c [ i ] ± 1 c[i]±1 c[i]±1时,答案会发生怎样的转化?
完全平方公式大家都会吧!!!
( c [ i ] − 1 ) 2 = c [ i ] 2 − 2 ∗ c [ i ] + 1 (c[i]-1)^2=c[i]^2-2*c[i]+1 (c[i]1)2=c[i]22c[i]+1
( c [ i ] + 1 ) 2 = c [ i ] 2 + 2 ∗ c [ i ] + 1 (c[i]+1)^2=c[i]^2+2*c[i]+1 (c[i]+1)2=c[i]2+2c[i]+1
学习莫队算法 (概念 + 时间复杂度分析 + 适用范围 + 例题:小B的询问)_第7张图片

代码实现

#include 
#include 
#include 
using namespace std;
#define LL long long
#define MAXN 50005
struct node {
	int l, r, num;
}G[MAXN];
int n, m, k, apart, curl = 1, curr; 
int a[MAXN], cnt[MAXN];
LL result;
LL ans[MAXN];

bool cmp ( node x, node y ) {
	return ( x.l / apart == y.l / apart ) ? x.r < y.r : x.l < y.l;
}

void add ( int x ) {
	result += ( cnt[a[x]] << 1 ) + 1;
	cnt[a[x]] ++;
}
void removed ( int x ) {
	result -= ( cnt[a[x]] << 1 ) - 1;
	cnt[a[x]] --;
}

int main() {
	scanf ( "%d %d %d", &n, &m, &k );
	for ( int i = 1;i <= n;i ++ )
		scanf ( "%d", &a[i] );
	apart = sqrt ( n );
	for ( int i = 1;i <= m;i ++ ) {
		scanf ( "%d %d", &G[i].l, &G[i].r );
		G[i].num = i;
	}
	sort ( G + 1, G + m + 1, cmp );
	for ( int i = 1;i <= m;i ++ ) {
		int l = G[i].l, r = G[i].r;
		while ( curl < l ) {
			removed ( curl ++ );
		}
		while ( curl > l ) {
			add ( -- curl );
		}
		while ( curr > r ) {
			removed ( curr -- );
		}
		while ( curr < r ) {
			add ( ++ curr );
		}
		ans[G[i].num] = result;
	}
	for ( int i = 1;i <= m;i ++ )
		printf ( "%lld\n", ans[i] );
	return 0;
}

做了一点代码上的小更改,真是不好意思!!感谢旁边的童鞋提醒

你可能感兴趣的:(莫队算法,学习博客)