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

    啥是莫队算法??

    莫队算法其实本质就是暴力。

    但是莫队算法在暴力的时候,规划好了每一次暴力的顺序,统筹安排暴力,可以有效地降低总时间。

    怎么做呢?

    首先看一道例题。

    P3901 数列找不同

    问题要我们判断每个区间里面是不是每个数都不一样。

    假设你是一个刚学会编程的人,不会任何数据结构,你会怎么办呢?

    有一种比较简单的思路是开一个桶,然后每次询问就清空桶,再把区间装进去,最后遍历一遍桶,看看有没有哪个桶里是有超过$1$个的。

    Anyway,我们也可以遍历一遍桶,统计出一共的不同种类的数,如果种类数等于$r-l+1$就说明每个数都不一样。

    这样我们就很接近莫队算法的思想了。

    1.相邻区间转移

    现在考虑,我们已经统计出了$[l,r]$,如何知道$[l,r+1]$的种类数。

    我们已经有了$[l,r]$中每种数的个数,我们把$a[r+1]$加入,如果发现$cnt[a[r+1]]$是空的,我们种类数$++$,然后$cnt[a[r+1]]++$

    这样子我们就能从$[l,r]$转移到$[l,r+1]$了。

    这种转移是$O(1)$的,同理,剩下几个边界加加减减都很好推出来。

    1 void add(int x){
    2     if(!cnt[a[x]])now++;
    3     cnt[a[x]]++;
    4 }
    5 void del(int x){
    6     --cnt[a[x]];
    7     if(!cnt[a[x]])now--;
    8 }
    View Code

    我们也可以通过位运算压一下位

    1 void add(int x){now+=!cnt[a[x]]++;}
    2 void del(int x){now-=!--cnt[a[x]];}
    View Code

    这个代码可以直接插在主函数里面,可以加快不少的时间。

    2.询问排序

    通过区间转移,我们可以很方便地用上一个询问求出的数据去求出下一个询问了。

    但是,如果两个询问之间距离很大,程序还是$O(nm)$的。

    我们可以离线回答询问,这样子就可以通过统筹回答,降低时间了。

    一个比较容易想到的方法是按照左端点的大小排序。

    但是这样子只要区间的大小交替变换就能卡掉了。

    比如说下面这组数据

    $[1,1000],[2,3],[3,1000],[4,4]$

    明显按照左端点排序是行不通的。

    我们可以按照块给左端点排序,左端点在同一个块内时右端点升序。

    只要分块合理,在块内移动所需要的时间是很小的,也就是说我们给左端点一定的容错性。

    在一定的范围内,左端点可以来回移动,这样子既保证了左端点的复杂度,右端点的复杂度也不会上天。

    在随机数据下,块的大小为$frac{n}{m imes frac{2}{2}}$时,时间复杂度比较优秀。

    我们排序之后跑暴力就十分快了。

    完整代码:

     1 #include <bits/stdc++.h>
     2 using namespace std;
     3 const int N=5e5+1009;
     4 struct Q{
     5     int l,r,id;
     6     Q(int aa=0,int bb=0,int cc=0){l=aa;r=bb;id=cc;}
     7 }q[N];
     8 int read(){
     9     char c;int num,f=1;
    10     while(c=getchar(),!isdigit(c))if(c=='-')f=-1;num=c-'0';
    11     while(c=getchar(), isdigit(c))num=num*10+c-'0';
    12     return f*num;
    13 }
    14 int n,m,a[N],block,ans[N];
    15 int cnt[N],now=0;
    16 bool cmp(Q a,Q b){
    17     return (a.l/block==b.l/block)?a.r<b.r:a.l/block<b.l/block;
    18 }
    19 void add(int x){now+=!cnt[a[x]]++;}
    20 void del(int x){now-=!--cnt[a[x]];}
    21 int main()
    22 {
    23     n=read();m=read();
    24     for(int i=1;i<=n;i++)a[i]=read();
    25     for(int j=1;j<=m;j++){
    26         q[j].id=j;
    27         q[j].l=read();
    28         q[j].r=read();
    29     }
    30     block=n/sqrt(m*2/3);
    31     sort(q+1,q+1+m,cmp);
    32     int nl=1,nr=1;
    33     add(1); 
    34     for(int i=1;i<=m;i++){
    35         int l=q[i].l,r=q[i].r;
    36         while(nr<r)add(++nr);
    37         while(nr>r)del(nr--);
    38         while(nl<l)del(nl++);
    39         while(nl>l)add(--nl);
    40         ans[q[i].id]=(now==(r-l+1));
    41     }
    42     for(int i=1;i<=m;i++)
    43         printf("%s
    ",ans[i]?"Yes":"No");
    44     return 0;
    45 }
    View Code


    这里有一个邪门优化,可以让复杂度除2。

    我们在按左端点分块排序的基础上,对奇偶编号的块交替升降序对右端点排序。

    也就是说奇数的时候右端点升序,偶数的时候右端点降序。

    这样可以玄学降低时间复杂度。

    为什么可以这么做呢,假设我们一开始升序,排序完之后,右端点在最右边。如果新的块内还是升序的话,我们就要先从右跑到左,然后再跑回去,这样是很慢的。

    为何不从右跑的左的时候顺便统计了呢?

    这样子,原来要跑两趟,现在只需要一趟就可以了,对时间的优化其实是很大的。

    时间复杂度为$O(nsqrt{m})$

    带修莫队和树上莫队留坑待补。。

  • 相关阅读:
    Spring工厂方法(factory-bean)配置bean
    subline关联linux系统
    第五篇 scrapy安装及目录结构,启动spider项目
    第八篇 编写spider爬取jobbole的所有文章
    第七篇 css选择器实现字段解析
    第六篇 xpath的用法
    爬虫 主要基础知识
    在ubuntu16下安装virtualenv+virtualenvwrapper
    git 和github简介
    git stash封存分支 以及关于开发新功能的处理
  • 原文地址:https://www.cnblogs.com/onglublog/p/10158669.html
Copyright © 2011-2022 走看看