我们首先来看一道例题:
Description
有n个数字,给出k,以及m个查询。
每次查询的格式是L,r,求L~r(左右包含)这个区间内数字的出现次数刚好是k的数字种数。
范围:n<=30000,k<=n,m<=30000,1<=L<r<=n,数列中元素大小<=n。
输入n,k,m,然后n个元素,然后m行查询,对于每一个询问,输出正确的答案。
Example input:
5 2 3
1 2 3 2 2
1 2
2 4
1 5
Example output:
0 //没有
1 //2
0 //没有(2太多了,也不算)
我们来考虑一下这题的解法。
首先肯定可以万能暴力,每次L~r枚举。时间复杂度O(N*M)。
但是这样的暴力是没有前途的!我们来考虑一下新的暴力:
一开始指针区间0->0,然后对于一个查询,我们将Left指针逐步更新成新的L,Right同理。
比如一开始Left=2,Right=3,而L=1,r=5。
那么我们Left-1,并且把Left位置上的数字出现次数+1.
Right+1,把Right位置上的数字出现次数+1,直到Right=5为止。
框架:
int add(x)//把x位置的数字加入进来
{
cnt[x]++;
if (cnt[x]==k) ans++;
}
int remove(x)//把x位置的数字移出去
{
cnt[x]--;
if (cnt[x]==k-1) ans--;
}
然后以上面题目为例;这种方法需要离线处理,我们同理来看一下解法:
Left=1;
Right=0;
ans=0;
for(int u=1;i<=b;u++)
{
while (Left<L[u]){ remove(Left); Left++;}
while (Left>L[u]){ Left--; add(Left);}
while (Right<r[u]){ Right++; add(Right};}
while (Right>r[u]){ remove(Right); Right--;}
printf("%d\n",ans);
}
这里说明一下,其实remove(Left);Left--;等等可以直接写成remove(Left--)等等,我这么写是为了理解。
分析一下时间复杂度,我们可以从Left和Right的移动量来分析:
每一个新的询问,Left和Right的移动量最大都会是O(N)
所以这样子的方法时间复杂度仍然是O(N*M),而且可能比上面的暴力更慢。
但是莫队算法的核心,就是从这么一个算法转变过来的。
现在来介绍一下莫队算法解决这道题:
对询问进行分块,我们知道m个询问,L和r的范围都在n以内,我们根据L和r的大小来对询问分块。
比如n=9,有以下的询问:
2 3
1 4
4 5
1 6
7 9
8 9
5 8
6 8
对于n=9,我们以根号n为每个块block的大小,这里block=3.
那么我们把1~3分成一组,4~6,7~9.
对于每一个询问(L,r),我们以L的范围来决定这个询问在哪一个块。
然后每一个独自的块内,我们让询问r更小的排在更前面。
那么上面的询问就可以分组成:
(2,3)/(1,4)/(1,6)和
(4,5)/(5,8)/(6,8)和
(7,9)/(8,9)
这一步的排序操作,我们可以在排序的时候加入判断条件cmp:
bool cmp(node x,node y){
if ((x/block)!=(y/block))
return x.L<y.L; //不同块的时候
return x.r<y.r; //同一块的时候
}
排序之后,我们再来分析一下时间复杂度;接下来我们会看到神奇的事情!!
刚才分析此方法的时候,我们是从L和R的偏移量分析的;我们仍然用这种方法来分析。
考虑一下在同一个块的时候。由于L的范围是确定的,所以每次L的偏移量是O(√N)
但是r的范围没有确定;r的偏移量是O(N)。
那么从一个块到另一个块呢?
明显地,r我们不需要作考虑,仍然是O(N)。
而L明显最多也是2*√N,而且这种情况下,很快就会到下下一块。所以也是O(√N)
由于有√N(根号N)个块,所以r的总偏移量是O(N*√N)
而M个询问,每个询问都可以让L偏移O(√N),所以L的总偏移量O(M*√N)
注意了,时间复杂度分析的时候一定要注意,r的偏移量和询问数目是没有直接关系的。
而L则恰恰相反;L的偏移量我们刚才也说明了,它和块的个数没有直接关系。
所以总的时间复杂度是:
O((N+M)*√N)
很神奇地看到了,我们仅仅改变了一下问题求解的次序,就让时间复杂度大幅度下降!
当然在这个说明过程中我们也看到了,事实上,莫队是一个必须离线的算法。
意味着一些题目如果强制在线,那么莫队就无能为力了。
先面贴一个莫队的典型题目
题目描述
作为一个生活散漫的人,小Z每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿。终于有一天,小Z再也无法忍受这恼人的找袜子过程,于是他决定听天由命……
具体来说,小Z把这N只袜子从1到N编号,然后从编号L到R(L 尽管小Z并不在意两只袜子是不是完整的一双,甚至不在意两只袜子是否一左一右,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。
你的任务便是告诉小Z,他有多大的概率抽到两只颜色相同的袜子。当然,小Z希望这个概率尽量高,所以他可能会询问多个(L,R)以方便自己选择。
然而数据中有L=R的情况,请特判这种情况,输出0/1。
输入输出格式
输入格式:
输入文件第一行包含两个正整数N和M。N为袜子的数量,M为小Z所提的询问的数量。接下来一行包含N个正整数Ci,其中Ci表示第i只袜子的颜色,相同的颜色用相同的数字表示。再接下来M行,每行两个正整数L,R表示一个询问。
输出格式:
包含M行,对于每个询问在一行中输出分数A/B表示从该询问的区间[L,R]中随机抽出两只袜子颜色相同的概率。若该概率为0则输出0/1,否则输出的A/B必须为最简分数。(详见样例)
输入样例
6 4
1 2 3 3 3 2
2 6
1 3
3 5
1 6
输出样例
2/5
0/1
1/1
4/15
说明
30%的数据中 N,M ≤ 5000;
60%的数据中 N,M ≤ 25000;
100%的数据中 N,M ≤ 50000,1 ≤ L < R ≤ N,Ci ≤ N。
题解
从所有袜子的数量中选取两个所组成的组合数作为分母,再从每种袜子的总数选取两个作为分子
所以分子为cnt[i]*(cnt[i]-1)的和 i为每种袜子 cnt记录的每种袜子的数量
分母为(r-l+1)*(r-l)
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cmath>
#include <math.h>
#include <cstring>
#include <string>
#include <queue>
#include <deque>
#include <stack>
#include <stdlib.h>
#include <list>
#include <map>
#include <utility>
#include <time.h>
#include <set>
#include <bitset>
#include <vector>
#define pi acos(-1.0)
#define inf 0x3f3f3f3f
#define linf 0x3f3f3f3f3f3f3f3f
#define ms(a,b) memset(a,b,sizeof(a))
#define INF 0x3f3f3f3f
#define ll long long
const int maxn=1e5+5;
using namespace std;
int read()
{int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}
ll qpow(ll x, ll y, ll mod){ll s=1;while(y) {if(y&1)s=s*x%mod;x=x*x%mod;y>>=1;}return s;}
int n,m,k;
ll val[50005];
ll cnt[50005];
ll a[50005];
int Left,Right;
ll sum=0;
int block;
struct node
{
ll a;
ll b;
int l,r;
int id;
}p[100005];
int cmp(node a,node b)
{
if((a.l/block)==(b.l/block))return a.r<b.r;
return a.l<b.l;
}
int cmp1(node a,node b)
{
return a.id<b.id;
}
int add(int x)//把x位置的数字加入进来
{
sum-=(cnt[val[x]])*(cnt[val[x]]-1);
cnt[val[x]]++;
sum+=cnt[val[x]]*(cnt[val[x]]-1);
}
int remove(int x)//把x位置的数字移出去
{
sum-=cnt[val[x]]*(cnt[val[x]]-1);
cnt[val[x]]--;
sum+=cnt[val[x]]*(cnt[val[x]]-1);
}
int main()
{
scanf("%d%d",&n,&m);
block=(int)sqrt(n);
for(int i=1;i<=n;i++)scanf("%lld",&val[i]);
for(int i=1;i<=m;i++)scanf("%d%d",&p[i].l,&p[i].r),p[i].id=i;
sort(p+1,p+m+1,cmp);
Left=1;
Right=0;
for(int i=1;i<=m;i++)
{
if(p[i].l==p[i].r){p[i].a=0,p[i].b=1;continue;}
while (Left<p[i].l){ remove(Left++);}
while (Left>p[i].l){ add(--Left);}
while (Right<p[i].r){ add(++Right);}
while (Right>p[i].r){ remove(Right--);}
p[i].a=sum;
p[i].b=(ll)(p[i].r-p[i].l+1)*(p[i].r-p[i].l);
ll gcd=__gcd(p[i].a,p[i].b);
p[i].a/=gcd,p[i].b/=gcd;
}
sort(p+1,p+m+1,cmp1);
for(int i=1;i<=m;i++)
printf("%lld/%lld\n",p[i].a,p[i].b);
return 0;
}