Processing math: 100%

莫队算法

以前经常听人说“离线莫队搞一搞”这种十分dalao的话,但是从来没有学习过这种奇妙的算法

说到底还是一种乱搞的方法,采用了平方分割的技巧

不过和块状链表那样的平方分割后维护块内内容不一样,莫队算法是通过某种顺序计算所有查询,使得均摊复杂度为O(NN)

 

比如,有一个长度为n的序列ai,其中的每一个元素有一个颜色ci

我们有m个查询[lj,rj],即查询这段区间内相同颜色最多出现多少次、一共有多少种颜色出现次数最多,这两个询问

我们显然能够用暴力的方法O(N2)地解决这个问题:对于每个[lj,rj],一个for循环跑一遍就行了

那么两个查询[lj,rj][lj,rj]之间是否存在某些关系,来帮助我们减少枚举次数呢?

事实上,如果我们已经知道[lj,rj]的结果,只需要将左端点lj一点点移到lj、将右端点rj一点点移到rj,我们就能得到查询[lj,rj]的结果

问题在于,从一个查询变为另一个,左右端点的移动可能是O(N)级别的

现在考虑将整个序列分成N个区间,则每个区间的长度是N

我们对于所有询问,处理的顺序是:将所有询问排序,第一关键字是lj所在块的编号,第二关键字是rj所在块的编号

为什么这样做能够保证均摊复杂度呢?

对于每组lj所在块编号和rj所在块编号对应相等的查询,因为所有左端点都在同一块内,所以最多移动N次;右端点同理

而从一组跳到另一组,对于lj所在块编号确定的时候,右端点从第1块一直跳到第N块,而相邻块之间的跳转跟块的长度有关;lj所在块编号也要跳N次,所以总体上进行了N×NO(N)的跳转

总而言之,就是通过减少相邻询问间的跳转来降低复杂度,而真正的计算仍然是暴力

 

给一道具体的题目吧:BZOJ 2038

在这道题目中,每次查询[lj,rj]的分母就是C(rjlj+1,2);而分子则是计算当前区间内每个数字i出现的次数cnti,并对每个iC(cnti,2)求和

想使用莫队算法,我们先得考虑如何暴力:即,怎么从一个查询移动到另一个

假设一次移动以后,我们加入了数字a,从而使其在现有区间内的出现次数从x变为x+1

那么数字a对分子的贡献由C(x,2)变为C(x+1,2),即由x(x1)2变为(x+1)x2,相当于增加了一个x

这样,我们每次移动的计算就是O(1)的了,接下来就是用上面莫队的思路按顺序处理所有询问

复制代码
#include <cstdio>
#include <cstring>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;

struct Query
{
    int x,y,id;
    Query(int a,int b,int c)
    {
        x=a,y=b,id=c;
    }
};

typedef long long ll;
const int MAX=50005;
const int SQ=240;

int sz;

inline int Index(int x)
{
    return x/sz;
}

inline bool operator < (Query a,Query b)
{
    if(Index(a.x)!=Index(b.x))
        return Index(a.x)<Index(b.x);
    return Index(a.y)<Index(b.y);
}

int n,m;
int a[MAX];
vector<Query> v;

int cnt[MAX];
ll ans1[MAX],ans2[MAX];

inline ll gcd(ll x,ll y)
{
    if(y==0)
        return x;
    return gcd(y,x%y);
}

int main()
{
//    freopen("input.txt","r",stdin);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    sz=(int)sqrt(n*1.0)+1;
    
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        v.push_back(Query(x,y,i));
    }
    sort(v.begin(),v.end());
    
    int left=v[0].x,right=left-1;
    ll val=0;
    for(int i=0;i<v.size();i++)
    {
        int x=v[i].x,y=v[i].y,id=v[i].id;
        while(left<x)
            --cnt[a[left]],val-=cnt[a[left]],left++;
        while(left>x)
            val+=cnt[a[--left]],cnt[a[left]]++;
        while(right<y)
            val+=cnt[a[++right]],cnt[a[right]]++;
        while(right>y)
            --cnt[a[right]],val-=cnt[a[right]],right--;
        ans1[id]=val,ans2[id]=(ll)(y-x+1)*(y-x+0)/2;
    }
    
    for(int i=1;i<=m;i++)
    {
        ll div=gcd(ans2[i],ans1[i]);
        printf("%lld/%lld\n",ans1[i]/div,ans2[i]/div);
    }
    return 0;
}
View Code
复制代码

 

树上莫队

一般的莫队只能处理序列上的问题,而树上的问题(特别是查询子树)一般会通过dfs序将树上问题转化成序列上问题

再给一道题目:CF 600E

在这题中,我们要对每个点进行一次查询

怎么转化成序列上问题呢?如果对这颗树从根开始进行一次dfs,并且记录访问每个点的顺序li和离开每个点的顺序ri,就能够发现:以某个点x为根的子树中,所有点的访问顺序都介于lxrx之间

复制代码
inline void dfs(int x,int fa)
{
    l[x]=++tot;
    for(int i=0;i<e[x].size();i++)
    {
        int next=e[x][i];
        if(next!=fa)
            dfs(next,x);
    }
    r[x]=tot;
}
复制代码

这样,我们就把一棵树通过dfs序拍平成了一个序列,剩下的就是序列上的明显的莫队了

复制代码
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;

struct Query
{
    int x,y,id;
    Query(int a=0,int b=0,int c=0)
    {
        x=a,y=b,id=c;
    }
};

inline bool operator < (Query a,Query b)
{
    return a.y<b.y;
}

typedef long long ll;
const int MAX=100005;
const int SQ=350;

int n;
int c[MAX];
vector<int> e[MAX];

int tot;
int l[MAX],r[MAX];

inline void dfs(int x,int fa)
{
    l[x]=++tot;
    for(int i=0;i<e[x].size();i++)
    {
        int next=e[x][i];
        if(next!=fa)
            dfs(next,x);
    }
    r[x]=tot;
}

int sz;
int a[MAX];
vector<Query> v[SQ];

ll sum[MAX];
int cnt[MAX];

inline int Index(int x)
{
    return x/sz+1;
}

ll ans[MAX];

int main()
{
//    freopen("input.txt","r",stdin);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&c[i]);
    for(int i=1;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        e[x].push_back(y);
        e[y].push_back(x);
    }
    
    dfs(1,0);
    for(int i=1;i<=n;i++)
        a[l[i]]=c[i];
    
    sz=(int)sqrt(n*1.0)+1;
    for(int i=1;i<=n;i++)
        v[Index(l[i])].push_back(Query(l[i],r[i],i));
    for(int i=1;i<=Index(n);i++)
        sort(v[i].begin(),v[i].end());
    
    for(int i=1;i<=Index(n);i++)
    {
        if(!v[i].size())
            continue;
        
        memset(sum,0LL,sizeof(sum));
        memset(cnt,0,sizeof(cnt));
        int left=v[i][0].x,right=left-1,top=0;
        for(int j=0;j<v[i].size();j++)
        {
            int x=v[i][j].x,y=v[i][j].y,id=v[i][j].id;
            while(left<x)
            {
                int color=a[left];
                sum[cnt[color]]-=color;
                cnt[color]--;
                sum[cnt[color]]+=color;
                left++;
                if(!sum[top])
                    top--;
            }
            
            while(left>x)
            {
                left--;
                int color=a[left];
                sum[cnt[color]]-=color;
                cnt[color]++;
                sum[cnt[color]]+=color;
                top=(cnt[color]>top?cnt[color]:top);
            }
            
            while(right<y)
            {
                right++;
                int color=a[right];
                sum[cnt[color]]-=color;
                cnt[color]++;
                sum[cnt[color]]+=color;
                top=(cnt[color]>top?cnt[color]:top);
            }
            
            ans[id]=sum[top];
        }
    }
    
    for(int i=1;i<=n;i++)
        printf("%I64d ",ans[i]);
    return 0;
}
View Code
复制代码

 

带修改莫队(三维莫队)

上面的所有莫队都是不带修改的,如果有修改存在,能否用莫队处理?

可以!(不过更加奇妙了)

我们的每次查询相当于带了一个时间维度ti,表示ti前的所有修改必须到位

那么我们重新考虑一下查询间的关系

[li,ri,ti][li,ri,ti],我们不仅需要移动左右端点,还必须考虑到时间上的移动

我们先可以将左右端点先移到[li,ri](方法跟上面是一样的),问题在于时间如何移动:如果在时间tx上有一个修改

  • 向后移:将颜色序列上的对应位置修改成新颜色,如果这个位置在当前区间内,就减去原颜色,加上新颜色
  • 向前移:将颜色序列上的对应位置恢复成原颜色,如果这个位置在当前区间内,就减去新颜色,加上原颜色

而我们要做的就是一直移动时间,将ti前的所有修改全部安排上,同时ti后的一点也不修改

这样,查询之间可以通过端点每次O(1)的移动来慢慢到达

而且处理查询的顺序也出来了:将所有查询排序,第一关键字是li所在块的编号,第二关键字是ri所在块的编号,第三关键字是ti

不过最玄学(其实很有道理)的地方来了:一共分为N13块,每块长度为N23,均摊复杂度为O(N53)

第一眼看上去是不是有点吓人,现在我们来分析为什么要这样划分

如果左右端点所在块确定了,左右端点在块内的移动是O(N23)的,n次查询都需要移动

时间的移动是单调的,在n13×n13个可能的左右端点所在块的分布中,都需要O(N)的扫描

对于左端点所在块确定的情况,右端点要跳n13次,每次跳转要花费O(N13)的右端点移动时间和O(N)的时间维移动时间;左端点一共跳n13次,一共是n13×n13O(N)的跳转

这样一来每部分都是O(N53)

 

扔一道UVa的裸题:UVa 12345

就是单纯的带修改莫队而已,跟上面分析的一样做就行了

好像自增自减纠缠在语句里会WA...以后要注意了

复制代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <cmath>
using namespace std;

struct Query
{
    int x,y,id;
    Query(int a,int b,int c)
    {
        x=a,y=b,id=c;
    }
};

struct Modify
{
    int x,y,prev,id;
    Modify(int a,int b,int c,int d)
    {
        x=a,y=b,prev=c,id=d;
    }
};

const int MAX=50005;
const int SQ=250;
int sz;

inline int Index(int x)
{
    return x/sz;
}

inline bool operator < (Query a,Query b)
{
    if(Index(a.x)!=Index(b.x))
        return Index(a.x)<Index(b.x);
    if(Index(a.y)!=Index(b.y))
        return Index(a.y)<Index(b.y);
    return a.id<b.id;
}

int n,m;
int c[MAX];

int a[MAX];
vector<Query> q;
vector<Modify> v;

int tot;
int cnt[MAX*20];

inline void Del(int x)
{
    cnt[a[x]]--;
    if(cnt[a[x]]==0)
        tot--;
}

inline void Add(int x)
{
    cnt[a[x]]++;
    if(cnt[a[x]]==1)
        tot++;
}

int ans[MAX];

int main()
{
//    freopen("input.txt","r",stdin);
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++)
        scanf("%d",&c[i]),a[i]=c[i];
    for(int i=1;i<=m;i++)
    {
        char op=getchar();
        while(op<'A' || op>'Z')
            op=getchar();
        int x,y;
        scanf("%d%d",&x,&y);
        
        if(op=='M')
            v.push_back(Modify(x,y,a[x],i)),a[x]=y;
        else
            q.push_back(Query(x,--y,i));
    }
    
    for(int i=1;i<=n;i++)
        if(i*i*i>=n)
        {
            sz=i*i;
            break;
        }
    for(int i=0;i<n;i++)//#2
        a[i]=c[i];
    sort(q.begin(),q.end());
    
    memset(ans,-1,sizeof(ans));
    int left=q[0].x,right=left-1,j=-1;
    for(int i=0;i<q.size();i++)
    {
        int x=q[i].x,y=q[i].y,id=q[i].id;
        while(left<x)
            Del(left++);
        while(left>x)
            Add(--left);
        while(right<y)//#1
            Add(++right);
        while(right>y)
            Del(right--);
        while(j+1<v.size() && v[j+1].id<id)
            if(v[++j].x>=x && v[j].x<=y)
                Del(v[j].x),a[v[j].x]=v[j].y,Add(v[j].x);
            else
                a[v[j].x]=v[j].y;
        while(j>=0 && v[j].id>id)
            if(v[j].x>=x && v[j].x<=y)
                Del(v[j].x),a[v[j].x]=v[j].prev,Add(v[j].x),j--;
            else
                a[v[j].x]=v[j].prev,j--;
        
        ans[id]=tot;
    }
    
    for(int i=1;i<=m;i++)
        if(ans[i]!=-1)
            printf("%d\n",ans[i]);
    return 0;
}
View Code
复制代码

 

以后应该还能用上,如果遇到的话再补一些题目上来

树上莫队:CF 375DTree and Queries

 

带修改莫队:Codeforces 940F (Machine Learning

区间中每个数出现数量的mex是很难用数据结构来维护的,因为相当于两层关系的叠加。所以考虑采用离线做法,就能够想到莫队了。

由于存在修改操作,所以应该采用有时间维度的带修改莫队,复杂度为O(n53)。我们有当前区间左右端点l,r和当前时间t,同时我们一定也维护了一个当前时间下的数组a[i],不妨记为cur[i]。对于所有修改这样处理:对于一次将位置p改为x的修改,我们先看p是否在l,r中(带修改莫队先移动l,r再移动t),如果不在的话令cur[p]=x就可以了,如果在的话需要先将cnt[cur[p]]减小、再令cur[p]=x、最后将cnt[x]增加。

一开始一直在想怎么O(1)动态维护mex,不过需要注意到一点,上面的O(n53)是所有l,r,t指针移动的复杂度,而实际的询问数仍是q。由于一共只有n个数,所以mex一定不超过n,于是对于每个查询都O(n)求一遍即可。总复杂度O(n53+nn)=O(n53)

复制代码
//18:54-19:20
//submits:
//19:20 (-1) TLE for index(id)
//19:23 (-2) n^2/3 for block size
#pragma GCC optimize(3)
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=100005;
const int sz=2500;

inline int index(int x)
{
    return x/sz;
}

struct Query
{
    int l,r,id;
    Query(int _l=0,int _r=0,int _id=0)
    {
        l=_l,r=_r,id=_id;
    }
};

inline bool operator <(const Query &X,const Query &Y)
{
    if(index(X.l)!=index(Y.l))
        return index(X.l)<index(Y.l);
    if(index(X.r)!=index(Y.r))
        if(index(X.l)%2==0)
            return index(X.r)<index(Y.r);
        else
            return index(X.r)>index(Y.r);
    return X.id<Y.id;
}

struct Modify
{
    int pos,val,prev,id;
    Modify(int _pos,int _val,int _prev,int _id)
    {
        pos=_pos,val=_val,prev=_prev,id=_id;
    }
};

int n,q;
int a[N],cur[N];
int op[N],x[N],y[N];

vector<int> vec;

inline int getpos(int x)
{
    return lower_bound(vec.begin(),vec.end(),x)-vec.begin()+1;
}

vector<Query> vq;
vector<Modify> vm;

int cnt[N<<1],mex[N<<1];

inline int getmex()
{
    int i=1;
    while(mex[i]>0)
        i++;
    return i;
}

inline void add(int x)
{
    if(cnt[x]>0)
        mex[cnt[x]]--;
    cnt[x]++;
    if(cnt[x]>0)
        mex[cnt[x]]++;
}

inline void del(int x)
{
    if(cnt[x]>0)
        mex[cnt[x]]--;
    cnt[x]--;
    if(cnt[x]>0)
        mex[cnt[x]]++;
}

int ans[N];

inline bool within(int L,int R,int pos)
{
    return (pos>=L && pos<=R);
}

int main()
{
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),vec.emplace_back(a[i]);
    for(int i=1;i<=q;i++)
    {
        scanf("%d%d%d",&op[i],&x[i],&y[i]);
        if(op[i]==2)
            vec.emplace_back(y[i]);
    }
    
    sort(vec.begin(),vec.end());
    vec.resize(unique(vec.begin(),vec.end())-vec.begin());
    
    for(int i=1;i<=n;i++)
        a[i]=getpos(a[i]),cur[i]=a[i];
    for(int i=1;i<=q;i++)
        if(op[i]==1)
            vq.emplace_back(Query(x[i],y[i],i));
        else
        {
            y[i]=getpos(y[i]);
            vm.emplace_back(Modify(x[i],y[i],cur[x[i]],i));
            cur[x[i]]=y[i];
        }
    
    sort(vq.begin(),vq.end());
    
    for(int i=1;i<=n;i++)
        cur[i]=a[i];
    
    int l=1,r=0,t=0;
    for(Query curq: vq)
    {
        int L=curq.l,R=curq.r,T=curq.id;
        while(l<L)
            del(cur[l]),l++;
        while(l>L)
            --l,add(cur[l]);
        while(r<R)
            ++r,add(cur[r]);
        while(r>R)
            del(cur[r]),r--;
        while(t<vm.size() && vm[t].id<T)
        {
            int pos=vm[t].pos;
            if(within(L,R,pos))
                del(cur[pos]),add(vm[t].val);
            cur[pos]=vm[t].val;
            t++;
        }
        while(t>0 && vm[t-1].id>T)
        {
            int pos=vm[t-1].pos;
            if(within(L,R,pos))
                del(cur[pos]),add(vm[t-1].prev);
            cur[pos]=vm[t-1].prev;
            t--;
        }
        
        ans[T]=getmex();
    }
    
    for(int i=1;i<=q;i++)
        if(op[i]==1)
            printf("%d\n",ans[i]);
    return 0;
}
View Code
复制代码

 

(完)

posted @   LiuRunky  阅读(348)  评论(0编辑  收藏  举报
编辑推荐:
· 理解Rust引用及其生命周期标识(下)
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
阅读排行:
· 2025成都.NET开发者Connect圆满结束
· 后端思维之高并发处理方案
· 千万级大表的优化技巧
· 在 VS Code 中,一键安装 MCP Server!
· 10年+ .NET Coder 心语 ── 继承的思维:从思维模式到架构设计的深度解析
点击右上角即可分享
微信分享提示