看了很多博客,终于看的差不多了
给定一个大小为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 :从当前集合内删除该位置的元素,并更新答案
那么这个时候莫队算法就重磅登场了
为什么叫做莫队算法了呢?
据说算法是由之前的国家队队长莫涛发明的,他的队友平日里称他为莫队,所以称之为莫队算法
莫队算法就是一个离线算法,仅仅调整了处理查询的顺序
实现过程如下↓:
将给定的输入数组分为 √ n √n √n块。每一块的大小为 n / √ n n/√n n/√n。
每个L落入其中的一块,每个R也落入其中的一块
如果某查询的L落在第i块中,则该查询属于第i块
所有的询问首先按照所在块的编号升序排列(所在块的编号是指询问的L属于的块)
如果编号相同,则按R值升序排列
莫队算法将依次处理第1块中的查询,然后处理第2块,最后直到第 n − √ n n-√n n−√n块
可以有很多的查询属于同一块。
例如:假设我们有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}
上述过程是正确,因为我们只是重新排列了查询的顺序
我们说了这么多,选用莫队算法无非就是想把时间复杂度给降下来
接下来我们来看看真正莫队的时间复杂度是多少,其实我看了很多博客也是有点懵逼
上面的代码就是起一个铺垫作用,
所有查询的复杂性是由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(n∗√n)
接下来看看左指针怎样移动
对于每个块,所有查询的左指针落在同一个块中,当我们从一个查询移动到另个一查询左指针会移动,
但由于前一个L与当前的L在同一块中,此移动是 O ( √ n ) O(√n) O(√n)(块的大小)
在每一块中左指针的移动总量是 O ( Q ∗ √ n ) O(Q∗√n) O(Q∗√n),(Q是落在那个块的查询的数量)
对于所有的块,总的复杂度为 O ( m ∗ √ n ) O(m∗√n) O(m∗√n)
综上,总复杂度为 O ( ( n + m ) ∗ √ n ) = O ( n ∗ √ n ) O((n+m)∗√n)=O(n∗√n) O((n+m)∗√n)=O(n∗√n)
首先莫队算法是一个离线算法,所以如果问题是在线操作带修或者强制特殊的顺序
莫队就失去了它的效应
其次一个重要的限制性:add和remove的操作
当有些题目的add和remove耗时很大, O ( √ N ) O(√N) O(√N)时就应该思考能否卡过
但是还是有很大一部分区间查询的题可以由莫队进行完成
所以高度总结概括:只有查询先想莫队,查询待修找线段树
小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]2−2∗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]2+2∗c[i]+1
#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;
}
做了一点代码上的小更改,真是不好意思!!感谢旁边的童鞋提醒