初探莫队

Posted lcez56jsy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初探莫队相关的知识,希望对你有一定的参考价值。

2019年的某月某天某神仙讲了莫队,但是我一直咕咕咕到了2020年

什么是莫队

莫队是一种优雅的暴力,也是用来完成区间询问的。普通莫队复杂度(O(n sqrt n))。一种十分优美的离线做法

前置芝士

0.拥有脑子

1.(STL)(sort)(cmp)

2.看/写超长的三目运算符的耐心

3.分块的思想

当然了如果不会这些也没有关系,下面还会再讲的

正片开始

先来一道卡了莫队的莫队模板题
HH的项链
技术图片
技术图片
最最暴力的做法:显然我们可以对每个询问暴力跑一次,但显然(O(n^2))跑不起。
在上面的暴力中,我们浪费了大量之前遍历过的区间的信息,现在考虑利用起这些信息。我们可以设置两个指针(l,r)表示当前所处的区间左端点和右端点。初始化(l=1,r=0)。(为了避免某些神奇的(RE))如果(l,r)不与询问区间的端点重合,就不断的跳(l,r)来更新答案。如果(l)在左端点右边,就不断向左跳,同时将(l)跳到的数统计进答案中,直到与左端点重合。如果(l)在左端点左边,就不断往右跳,同时将曾经待过的点从答案中删掉。对于这个题来说,可以用(cnt[x])表示(x)这个数出现的次数,如果某次增加时,(cnt[x]==0),(ans)(+1),如果某次删除时发现删完后(cnt[x]==0),(ans)(-1)

我们发现上面这个优化对于这种图来说效率极高:
技术图片
其中(x_i)表示第(i)次询问对应的区间

但是对于这种数据来说就凉了
技术图片
上面的优化方式在(x_4)里面不断得左右来回跳,导致浪费了大量的时间。

所以我们不妨把询问的区间进行排序。这样做就必须离线了。怎么排序呢?按照左端点单调递增?显然右端点无序会让这个优化只增加(O(nlogn))的排序复杂度。这时候,就要用到分块思想了。

我们把整个序列分成(sqrt n)个块,按照(l)所在的块升序排列为第一关键字,(r)升序排列为第二关键字排序。感觉好像没有什么用诶?但确实是个极大的优化至于为什么我也不知道

代码如下:

struct Q{
   int l,r,id,nub;//nub表示左端点在哪个块里
}qry[200009];
bool cmp(Q a,Q b)
{
    if(a.nub!=b.nub) return a.nub<b.nub;
    return a.r<b.r; 
}

当然卡常一点也可以写成这样:

bool cmp(Q a,Q b)
{ 
   return (a.nub^b.nub)?a.nub<b.nub:a.r<b.r;
}

过莫队板子的必备技能是卡常
这样基本的莫队就撒花完结了。

因为这道板子题卡了莫队,所以请走数据弱化版D_QUERY
板子题代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline ll read()
{
    char ch=getchar();
    ll x=0;bool f=0;
    while(ch<'0'||ch>'9')
    {
        if(ch=='-') f=1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
    {
        x=(x<<3)+(x<<1)+(ch^48);
        ch=getchar();
    }
    return f?-x:x;
}
int n,q,a[30009],ans[200009],cnt[1000009],all;
struct Q{
    int l,r,nub,id;
}qry[200009];
bool cmp(Q a,Q b)
{
    if(a.nub!=b.nub) return a.nub<b.nub;
    return a.r<b.r; //由于这题不卡常所以就没有卡
}
void add(int k)
{
    if(!cnt[a[k]]) all++;
    cnt[a[k]]++;
}
void del(int k)
{
    cnt[a[k]]--;
    if(!cnt[a[k]]) all--;
}
int main()
{
    n=read();
    for(int i=1;i<=n;i++)
     a[i]=read();
    q=read();
    int sn=sqrt(n);
    for(int i=1;i<=q;i++)
    {
        qry[i].id=i;qry[i].l=read();qry[i].r=read();
        qry[i].nub=qry[i].l/sn+1;
        if(qry[i].l%sn==0) qry[i].nub--;
    }   
    sort(qry+1,qry+1+q,cmp);
    int l=1,r=0;
    for(int i=1;i<=q;i++)
    {
        while(r<qry[i].r) add(++r);
        while(r>qry[i].r) del(r--);
        while(l<qry[i].l) del(l++);
        while(l>qry[i].l) add(--l);
        ans[qry[i].id]=all;
    }  
    for(int i=1;i<=q;i++)
     printf("%d
",ans[i]);
}

莫队的玄学优化

奇偶性排序

虽然上面的排序方法优化很大,但是能不能更快一点以便卡过毒瘤题呢?
方法当然是有的辣。
我们先来康康按照上面的排序方法会排出来个啥
技术图片
这是一堆询问区间以及并不优美的块的分界线
排序后:
技术图片
这样左端点跳动幅度不大,右端点在同一个块内也是递增的。但是当(r)从一个块跳到下一个块的时候发现有时候会倒退回来好多,然后又要重新向右跳。是不是有点浪费?所以奇偶性排序就是在奇数块内右端点按升序排序,偶数块内右端点按降序排序,这样右端点在往回跳的时候就能顺带跳完偶数块的询问。理论上能快一半

上面的按照奇偶性排序:
技术图片
手动模拟(r)的跳跃发现真的优化了不少
代码:

bool cmp(Q a,Q b)
{
  return (a.nub^b.nub)?(a.nub<b.nub):((a.nub%2)?a.r<b.r:a.r>b.r);
}
乱七八糟系列

(pragma GCC optimize(2),pragma GCC optimize (3),register),快读快输,(inline),把(for)里的(i++)换成(++i),用三目运算符代替blabla(待会卡带修莫队板子要用)

带修莫队

现在毒瘤出题人要求修改,怎么办呢?
就像这道题:数颜色
技术图片
技术图片
在很久很久以前,这道题是可以拿树套树卡过的你甚至只用去搞搞set,但是现在拿带修莫队都要吸氧了(qaq)

好了我们回到正题。
我们只需要在原来的莫队的基础上再加一维时间轴。将询问和修改分开存储。如果这次询问的时间在当前时间之后,就不断修改,直到时间相同。如果询问时间在当前时间之前,就再改回去,我们可以用(swap)做到,从而不用再开变量维护原来的值。

当然了,排序方式也有变化。这次我们按照(l)所在的块为第一关键字,(r)所在的块为第二关键字,时间为第三关键字进行排序。同时,奇偶性排序也不再适用。
排序:

bool cmp(Q a,Q b)
{
    return (bl[a.l]^bl[b.l])?bl[a.l]<bl[b.l]:((bl[a.r]^bl[b.r])?bl[a.r]<bl[b.r]:a.ti<b.ti);
}

注意块的大小会对复杂度有着极大的影响。据大佬证明当块的大小为(n^{frac{3}{4}})时,复杂度最优。但是我不会证。

由于这个题窝太菜了,不拿(O_2)实在是卡不过去,所以只好放上一份加(O_2)的代码了

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline int read()
{
    char ch=getchar();
    int x=0;bool f=0;
    while(ch<'0'||ch>'9')
    {
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
    {
        x=(x<<3)+(x<<1)+(ch^48);
        ch=getchar();
    }
    return f?-x:x;
}
int n,k,q,a[133339],bl[133339],ans[133339],cnt[1000009];
int all;
struct Q{
    int l,r,ti,id;
}qry[133339];
struct M{
    int p;
    int col;
}mdi[133339];
bool cmp(Q a,Q b)
{
    return (bl[a.l]^bl[b.l])?bl[a.l]<bl[b.l]:((bl[a.r]^bl[b.r])?bl[a.r]<bl[b.r]:a.ti<b.ti);
}
inline void add(int k)
{
    if(!cnt[a[k]]) all++;
    cnt[a[k]]++;
}
inline void del(int k)
{
    cnt[a[k]]--;
    if(!cnt[a[k]]) all--;
}
inline void modi(int i,int ti)
{
    if(mdi[ti].p>=qry[i].l&&mdi[ti].p<=qry[i].r)
    {
        int x=--cnt[a[mdi[ti].p]];
        int y=++cnt[mdi[ti].col];
        if(!x) all--;
        if(y==1) all++;
    }
    swap(a[mdi[ti].p],mdi[ti].col);
}
int main()
{
    n=read();q=read();
    for(int i=1;i<=n;i++)
     a[i]=read();
    int qc=0,mc=0;
    for(int i=1;i<=q;i++)
    {
        char k=getchar();
        while(k!='Q'&&k!='R') k=getchar();
        if(k=='Q')
        {
            qry[++qc].l=read();qry[qc].r=read();
            qry[qc].ti=mc;qry[qc].id=qc;
        }
        if(k=='R')
        {
            mdi[++mc].p=read();mdi[mc].col=read();
        }
    }   
    int sn=pow(n,3.0/4.0);
    for(int i=1;i<=n;i++)
    {
        bl[i]=(i-1)/sn+1;
    }
    sort(qry+1,qry+1+qc,cmp);
    int now=0,l=1,r=0;
    for(int i=1;i<=qc;i++)
    {
        while(r<qry[i].r) add(++r);
        while(r>qry[i].r) del(r--);
        while(l<qry[i].l) del(l++);
        while(l>qry[i].l) add(--l);
        while(now<qry[i].ti) modi(i,++now);
        while(now>qry[i].ti) modi(i,now--);
        ans[qry[i].id]=all;
    }
    for(int i=1;i<=qc;i++)
     printf("%d
",ans[i]);
}
 

以上是关于初探莫队的主要内容,如果未能解决你的问题,请参考以下文章

莫队算法&#183;初探总结

算法莫队算法初探

HDU_1175_莫队+逆元

初探JSP与LEeclipse

shader开发_4.Surface shader初探

莫队小结