浅谈莫对算法

我们首先来看一道例题:

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再也无法忍受这恼人的找袜子过程,于是他决定听天由命……

具体来说,小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;

}

 

 

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/qq_41021816/article/details/81502927