• 莫队算法


    最近一段时间不知道学什么,索性就按着学长的课件查漏补缺,补充一下自己以前漏掉的知识点

    莫队这个优秀的算法还是我们CJ学长莫涛队长发明的哩

    普通莫队

    先上道例题

    给出一个长度为n 的数列,q次询问,每次询问一个区间中多少种不同的数字

    这题显然可以用其他区间问题的数据结构来做,比如树状数组啥的
    原题链接

    但是!!!

    为什么不 (问问神奇海螺呢) 试试暴力呢

    让我们一起来分析一下暴力的写法:
    每次遍历询问区间,统计出答案

    那让我们想想,为什么这样的暴力跑不过呢?
    因为询问的区间有很多重复的片段,导致某些点不断被遍历,大大减缓了效率

    那我们是不是可以优化这个暴力???

    当然

    既然有很多重复的片段,那不是只要记录这个片段里保留的值就好了吗???
    那我们该怎样在保证正确性的同时来记录这个值呢?

    我们可以直接扩张或减小上一个询问区间来求得当前区间的值嘛,
    这样就可以少算一部分重复的区间了

    可是,这样的复杂度还是不够优秀,很容易被卡成O(N*Q)

    想想为什么会被卡成O(N*Q)呢?

    因为上一个询问区间和当前询问区间的两端点隔得太远,导致还是相当于几乎遍历了整个数组

    所以

    我们只要把上一个区间和当前区间的端点距离变近就好了!
    所以把所有询问区间先排序 (这也是为什么普通莫队处理不了待修改的问题)
    然后在按着询问来扫

    那么我们剩下的问题就是在按照什么顺序来排序了

    假如直接就按端点双关键字排序
    那么在最坏的情况下就会: 左端点只遍历一遍数组,右端点却要每次询问都遍历一遍区间,那么又是O(N*Q)

    所以我们不妨运用分块的思想, 将左端点按照所属的块来排序,右端点正常排序

    // 也就是这样
    int cmp(block a,block b)
    {
        if(a.l/sqrt(n)==b.l/sqrt(n)) return a.r<b.r;
        return a.l<b.l; //已经保证了不在同一个块内,不用再除以sqrt(n)了
    }
    

    为什么这样复杂度是对的呢?

    让我们分别考虑左右端点的移动情况:

    左端点 : 每次跨越在一个块或者两个块,有Q次移动,就是O(Q*sqrt(N));

    右端点 : 由于是在块内排序,所以每个块最多把整个序列遍历一遍,就是O(N*(sqrt(N)))

    #include<bits/stdc++.h>
    using namespace std;
    #define re register
    #define ll long long
    #define get getchar()
    #define in inline
    in ll read()
    {
        ll t=0; char ch=get;
        while(ch<'0' || ch>'9') ch=get;
        while(ch<='9' && ch>='0') t=t*10+ch-'0',ch=get;
        return t;
    }
    const int _=1e6+6;
    int n;
    ll m,a[_],piece,vis[_],ans=0,answer[_];
    struct block{
        int l,r,id;
    }q[_];
    in int cmp(block a,block b)
    {
        if(a.l/piece == b.l/piece)
            return a.r<b.r;
        return a.l<b.l;
    }
    in void del(int x)
    {
        vis[x]--;
        if(vis[x]==0)ans--;
    }
    in void add(int x)
    {
        if(vis[x]==0)ans++;
        vis[x]++;
    }
    int main()
    {
        n=read();
        for(re int i=1;i<=n;i++)
            a[i]=read();
        m=read();
        for(re int i=1;i<=m;i++)
            q[i].l=read(),q[i].r=read(),q[i].id=i;
        piece=sqrt(n);
        sort(q+1,q+m+1,cmp);
        int l=1,r=0;
        for(re int i=1;i<=m;i++)
        {
            while(l<q[i].l) del(a[l]),l++;
            while(l>q[i].l) add(a[l - 1]),l--;
            while(r<q[i].r) add(a[r + 1]),r++;
            while(r>q[i].r) del(a[r]),r--;
            answer[q[i].id]=ans;
     	}
        for(re int i=1;i<=m;i++)
            cout<<answer[i]<<endl;
    }
    
    

    带修改莫队

    又是一道例题

    Luogu P1903 数颜色
    给定一个长为 (n) 的序列,与m个操作,一种为单点修改,一种为询问区间中有多少个不同的数字,T组询问

    思路

    看到这个题面,想想可以用哪些方法来解决?
    刚刚我们不是学了莫队吗,那我们想想如何用莫队来解决这个问题

    首先

    之前我们学到了,莫队只适合在无修改且可离线的情况下使用

    那我们有什么办法在莫队中加入修改操作呢???

    我们一起来想想普通莫队为什么不支持修改操作,
    显然,是因为莫队需要排序,而排序之后修改操作的顺序就乱了.

    然后

    为何不记录一下每次询问前是第几个修改操作呢?

    并且在排序时把这个 "时间戳" 作为第三关键字

    in int cmp(qu a,qu b)
    {
    	if((a.l-1)/block==(b.l-1)/block) // block为莫队中每一个询问块的大小
    	{
    		if((a.r-1)/block==(b.r-1)/block)return a.t<b.t; // t为时间戳
    		return a.r<b.r;
    	}
    	else return a.l<b.l;
    }
    

    每次询问都相应的更新或恢复之前的值

    // 莫队单点修改函数
    void change(int i,int l,int r,bool flag) //第i个修改操作,当前询问区间 L~R 
    {     // flag为0表示当前位置需要修改为操作要求的值,为1表示需要恢复成此次操作之前的样子
    	int x=modify[i].x;
    	if(x>=l&&x<=r)
      		del(x); //把当前颜色对答案的影响删掉
    	c[x]=flag?modify[i].now:modify[i].last;
    	if(x>=l&&x<=r)
    		add(x); //把修改后的颜色对答案的影响加入
    }
    

    附上此题完整代码 YZHX太胖了,常数也大,要开O2才能过:

    #include<bits/stdc++.h>
    using namespace std;
    #define re register
    #define ll long long
    #define in inline
    #define get getchar()
    in int read()
    {
        int t=0; char ch=get;
        while(ch<'0' || ch>'9') ch=get;
        while(ch<='9' && ch>='0') t=t*10+ch-'0',ch=get;
        return t;
    }
    const int _=1e5+5;
    struct qu{
        int l,r,t,id;
    }a[_];  // 询问操作
    struct mo{
        int x,now,last;
    }modify[_]; //修改操作, now是修改前的数字,last是修改后的,x记录修改位置
    int n,m,sum[_*10],c1[_],c[_],qtot,rtot,block,ans,s[_];
    in void add(int x) //加入
    {
        sum[c[x]]++;
        if(sum[c[x]]==1)ans++;
    }
    in void del(int x) //删除
    {
        sum[c[x]]--;
        if(sum[c[x]]==0)ans--;
    }
    in int cmp(qu a,qu b) //排序顺序 
    {
        if((a.l-1)/block==(b.l-1)/block)
        {
            if((a.r-1)/block==(b.r-1)/block)return a.t<b.t;
            return a.r<b.r;
        }
        else return a.l<b.l;
    }
    in void change(int i,int l,int r,bool flag) //修改
    {
        int x=modify[i].x;
        if(x>=l&&x<=r)
            del(x);
        c[x]=flag?modify[i].now:modify[i].last;
        if(x>=l&&x<=r)
            add(x);
    }
    int main()
    {
        n=read(),m=read();
        block=sqrt(n);
        for(re int i=1;i<=n;i++)
            c1[i]=c[i]=read(); //需要 c1 数组记录序列初始状态
        for(re int i=1;i<=m;i++)
        {
            char ch=get;
            while(ch!='Q'&&ch!='R')ch=get;
            int x=read(),y=read();
            if(ch=='Q') a[++qtot].l=x,a[qtot].r=y,a[qtot].t=rtot,a[qtot].id=qtot;
            else modify[++rtot].x=x,modify[rtot].now=c[x],modify[rtot].last=y,c[x]=y;
        }
        for(re int i=1;i<=n;i++) c[i]=c1[i];
        sort(a+1,a+qtot+1,cmp);
        int l,r,tnow;
        l=a[1].l,r=a[1].r,tnow=a[1].t;
        for(re int i=l;i<=r;i++)
            add(i);
        for(re int i=1;i<=tnow;i++)
            change(i,l,r,0);
        s[a[1].id]=ans;
        for(re int i=2;i<=qtot;i++)
        {
            while (l<a[i].l) del(l++);
            while (l>a[i].l) add(--l);
            while (r<a[i].r) add(++r);
            while (r>a[i].r) del(r--);
            while (tnow<a[i].t) change(++tnow,l,r,0);
            while (tnow>a[i].t) change(tnow--,l,r,1);
            s[a[i].id]=ans;
        }
        for(re int i=1;i<=qtot;i++)
            cout<<s[i]<<endl;
    }
    
    
  • 相关阅读:
    删除MSSQL危险存储过程的代码
    给年轻工程师的十大忠告[转贴]
    HTML中利用堆栈方式对Table进行行排序
    年轻人宣言:青春符号
    刘亦菲小龙女绝美剧照
    精巧完整的日历程序
    XSLT快速参考
    酒吧里经典的英文歌曲专集(4CD)
    检测系统颜色与使用字体
    SQL Server实用操作小技巧集合
  • 原文地址:https://www.cnblogs.com/yzhx/p/11236569.html
Copyright © 2020-2023  润新知