zoukankan      html  css  js  c++  java
  • 浅谈莫队算法

    首先来看一道例题:

    Description
    有n个数字,以及m个查询。每次查询的格式是L,r,求L~r(左右包含)这个区间内有多少个不同的数?
    1< n,m<=50000,1<=L<r<=n,数列中元素大小<=n 。

    思路1:

    自然而然想到暴力,对每次询问L~r枚举区间,如果数列中元素取值范围很大,先要进行离散化处理。
    时间复杂度:O(nm)
    代码略

    思路2:

    线段树或树状数组处理一下。(代码复杂度相对而言较高)。

    #include<cstdio>
    #include<algorithm>
    #define lowbit(x) ((x)&(-(x)))
    using namespace std;
    int n,m;
    int b[500010],c[500010],last[1000010],ans[500010];
    struct node{int x,y,id;} a[500010];
    bool cmp(node x,node y)
    {
    	return x.y==y.y?x.x<y.x:x.y<y.y;
    }
    void add(int x,int y)
    {
    	while(x<=n)
    	{
    		c[x]+=y;
    		x+=lowbit(x);
    	}
    }
    int getsum(int x)
    {
    	int sum=0;
    	while(x)
    	{
    		sum+=c[x];
    		x-=lowbit(x);
    	}
    	return sum;
    }
    int main()
    {
    	scanf("%d",&n);
    	for(int i=1;i<=n;i++)
    		scanf("%d",&b[i]);
    	scanf("%d",&m);
    	for(int i=1;i<=m;i++)
    	{
    		scanf("%d %d",&a[i].x,&a[i].y);
    		a[i].id=i;
    	}
    	sort(a+1,a+m+1,cmp);
    	int now=1;
    	for(int i=1;i<=n;i++)
    	{
    		if(last[b[i]]) add(last[b[i]],-1);
    		last[b[i]]=i;
    		add(i,1);
    		while(i==a[now].y&&now<=m)
    		{
    			ans[a[now].id]=getsum(a[now].y)-getsum(a[now].x-1);
    			now++;
    		}
    		if(now==m+1) break;
    	}
    	for(int i=1;i<=m;i++)
    		printf("%d
    ",ans[i]);
    	return 0;
    }
    

    思路3

    回到思路1,方法1这样的暴力是没有前途的!我们来考虑一下新的暴力:
    一开始指针区间0->0,然后对于一个查询,我们将Left指针逐步更新成新的L,Right同理。
    比如一开始Left=2,Right=3,而当前查询 L=1,r=5。
    那么我们Left-1,并且把Left位置上的数字出现次数+1.
    Right+1,把Right位置上的数字出现次数+1,直到Right=5为止。

    add(x){  //把x位置的数字加入进来
        cnt[x]++;
        if (cnt[x]==0) ans++;
    }
    remove(x){  //把x位置的数字移出去
        cnt[x]--;
        if (cnt[x]!=0) ans--;
    }
    

    以上面题目为例;这种方法需要离线处理,我们同理来看一下解法:

    Left=Right=1; add(1);
    ans=0;
    for u=1 to m{
        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--;}
        output ans;
    }
    

    这里说明一下,其实remove(Left); Left--; 等等可以直接写成remove(Left--)等等,这么写是为了理解。
    分析一下时间复杂度,我们可以从Left和Right的移动量来分析:
    每一个新的询问,Left和Right的移动量最大都会是O(N)。所以这样子的方法时间复杂度仍然是O(NM),而且可能比上面的暴力更慢。
    但是莫队算法的核心,就是从这么一个算法转变过来的。
    现在来介绍一下莫队算法解决这道题:
    对询问进行分块,我们知道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 (block[x.x]==block[y.x])
            return x.r<y.r;        //同一块的时候
        return x.L<y.L;      //不同一块的时候
    }
    

    排序之后,我们再来分析一下时间复杂度;接下来我们会看到神奇的事情!!
    刚才分析此方法的时候,我们是从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)
    很神奇地看到了,我们仅仅改变了一下问题求解的次序,就让时间复杂度大幅度下降!
    当然在这个说明过程中我们也看到了,事实上,莫队是一个必须离线的算法。
    意味着一些题目如果强制在线,那么莫队就无能为力了。
    实现代码:

    #include<cstdio>
    #include<cmath>
    #include<algorithm>
    using namespace std;
    int n,m,now;
    int p[500010],block[500010],cnt[500010],ans[500010];
    struct node{int x,y,id;} a[500010];
    bool cmp(node x,node y)
    {
    	return block[x.x]==block[y.x] ? x.y<y.y : x.x<y.x;
    }
    void init()
    {
    	int u=sqrt(n);
    	for(int i=1;i<=n;i++)
    		block[i]=(i-1)/u+1;
    }
    void move(int x,int d)
    {
    	if(d)
    	{
    		if(!cnt[p[x]]) now++;
    		cnt[p[x]]++;
    	}
    	else
    	{
    		cnt[p[x]]--;
    		if(!cnt[p[x]]) now--;
    	}
    }
    int main()
    {
    	scanf("%d",&n);
    	for(int i=1;i<=n;i++)
    		scanf("%d",&p[i]);
    	scanf("%d",&m);
    	init();
    	for(int i=1;i<=m;i++)
    	{
    		scanf("%d%d",&a[i].x,&a[i].y);
    		a[i].id=i;
    	}
    	sort(a+1,a+m+1,cmp);
    	int l=0,r=0;
    	for(int i=1;i<=m;i++)
    	{
    		while(l<a[i].x) move(l++,0);
    		while(l>a[i].x) move(--l,1);
    		while(r<a[i].y) move(++r,1);
    		while(r>a[i].y) move(r--,0);
    		ans[a[i].id]=now;
    	}
    	for(int i=1;i<=m;i++)
    		printf("%d
    ",ans[i]);
    	return 0;
    }
    

    莫队算法小结

    莫队算法的条件
    1、不包含修改操作。
    2、题目允许离线,也就是允许在所有询问全部读入完之后回答所有询问。
    3、不同区间的结果可以互相计算得出。怎么理解这个条件呢?
    就上面的问题而言,如果上一次已经回答了[l,r]区间的答案,并且已经存下了[l,r]区间里的所有数值的出现次数,那么如果下面要询问[l,r+1]的结果,就只要把右指针r向右移一个单位,并将序列的第r+1个数的出现次数++,同时维护当前的答案,也就是说,如果第r+1个数在[l,r]区间内没有出现,则当前答案++。同样,对于[l,r−1],[l−1,r][l+1,r]以及其他任何的区间都可以在上一个询问的基础上,通过l和r移动指针来求得下一个询问的答案。如果满足这样的条件,就是说不同区间的结果可以互相计算得出。

    莫队算法的流程
    可以看出,一次移动指针是O(1)的。于是想到可以回答第1个询问之后,不断地移动指针,一个一个移动到后面将要回答的所有区间。
    莫队算法的主要思想就是这样。同时,莫队算法利用了可以离线的条件,将询问按照合理的顺序进行求解,实现了O(N√N)的复杂度。
    首先,将序列分块,即分成√n块,每个块的大小为√n。
    然后,就将询问按照左端点所在的块为第一关键字,右端点的位置为第二关键字进行从小到大排序,这样,就能像上面那样不断移动指针,可以达到O(N√N)的复杂度。

    复杂度证明
    不妨把左端点在同一个块内的询问分成一组。
    先考虑右端点的移动次数。由于在同一组询问内的右端点是递增的,所以在同一组内,右端点移动了O(n)次。同时在跨越两个组时,右端点的移动次数也是O(n),即右端点一共移动了O(N√N)次。
    再考虑左端点的移动次数。可以看出,在同一组询问内,左端点一次移动的次数为O(N√N)次。
    再加上在跨越两个组时,左端点的移动次数也是O(N√N),因此左端点一共移动了O(N√N)次。复杂度得证。

    巩固练习
    [BZOJ1878][SDOI2009]HH的项链
    [BZOJ2038][2009国家集训队]小z的袜子
    [BZOJ3236][AHOI2013]作业
    [BZOJ4540][HNOI2016]序列
    [BZOJ4542][HNOI2016]大数

  • 相关阅读:
    使用requests爬虫简单获取知乎问题信息
    slam介绍
    move_base 控制机器人(2)
    move_base 控制机器人(1)
    Linux 常用命令-touch
    Linux 常用命令-rmdir
    Linux 常用命令-rm
    Linux 常用命令-mkdir
    Linux 目录结构
    Linux 常用命令-pwd
  • 原文地址:https://www.cnblogs.com/tham/p/12200156.html
Copyright © 2011-2022 走看看