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

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

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

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

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

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

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

    那么两个查询$[l_j,r_j]$、$[l_{j'},r_{j'}]$之间是否存在某些关系,来帮助我们减少枚举次数呢?

    事实上,如果我们已经知道$[l_j,r_j]$的结果,只需要将左端点$l_j$一点点移到$l_{j'}$、将右端点$r_j$一点点移到$r_{j'}$,我们就能得到查询$[l_{j'},r_{j'}]$的结果

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

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

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

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

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

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

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

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

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

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

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

    那么数字$a$对分子的贡献由$C(x,2)$变为$C(x+1,2)$,即由$frac{xcdot (x-1)}{2}$变为$frac{(x+1)cdot x}{2}$,相当于增加了一个$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
    ",ans1[i]/div,ans2[i]/div);
        }
        return 0;
    }
    View Code

    树上莫队

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

    再给一道题目:CF 600E

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

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

    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

    带修改莫队(三维莫队)

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

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

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

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

    从$[l_i,r_i,t_i]$到$[l_{i'},r_{i'},t_{i'}]$,我们不仅需要移动左右端点,还必须考虑到时间上的移动

    我们先可以将左右端点先移到$[l_{i'},r_{i'}]$(方法跟上面是一样的),问题在于时间如何移动:如果在时间$t_x$上有一个修改

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

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

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

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

    不过最玄学(其实很有道理)的地方来了:一共分为$N^{frac{1}{3}}$块,每块长度为$N^{frac{2}{3}}$,均摊复杂度为$O(N^{frac{5}{3}})$

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

    如果左右端点所在块确定了,左右端点在块内的移动是$O(N^{frac{2}{3}})$的,$n$次查询都需要移动

    时间的移动是单调的,在$n^{frac{1}{3}} imes n^{frac{1}{3}}$个可能的左右端点所在块的分布中,都需要$O(N)$的扫描

    对于左端点所在块确定的情况,右端点要跳$n^{frac{1}{3}}$次,每次跳转要花费$O(N^{frac{1}{3}})$的右端点移动时间和$O(N)$的时间维移动时间;左端点一共跳$n^{frac{1}{3}}$次,一共是$n^{frac{1}{3}} imes n^{frac{1}{3}}$次$O(N)$的跳转

    这样一来每部分都是$O(N^{frac{5}{3}})$

    扔一道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
    ",ans[i]);
        return 0;
    }
    View Code

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

    树上莫队:CF 375D($Tree$ $and$ $Queries$)

    (完)

  • 相关阅读:
    使用CuteFTP登陆FTP(servU)服务器后无法LIST目录和文件的解决方法
    delphi技巧集锦之一
    delphi技巧集锦之二
    关于varchar(max), nvarchar(max)和varbinary(max)
    别告诉我你会用WORD
    Showmodal与show的区别
    SET ANSI_NULLS ON的用法
    {右键我的电脑无法打开计算机管理}解决方法
    Word常用技巧
    Excel 使用技巧集锦——163种技巧
  • 原文地址:https://www.cnblogs.com/LiuRunky/p/Mos_Algorithm.html
Copyright © 2011-2022 走看看