【莫队算法】总结&CF940F Machine Learning

题意:

给出一个长度为N序列(为什么D、E、F都是序列)
需要支持两种操作:
1、定义一个区间的值为:这段区间任意元素出现次数的集合的mex,给出l,r求原串中[l,r]这段区间的值
2、修改某个点的值
对mex的定义与SG函数中是相同的,表示一个自然数集中未出现的最小的整数。
例如: 1 、 3 、 2 、 1 、 2 、 2 、 2 1、3、2、1、2、2、2 1321222这个序列的值为3:
3出现了1次,1出现了2次,2出现了4次,集合中的数为 0 , 1 , 2 , 4 0,1,2,4 0,1,2,4(所有除了这三个数以外的 [ 0 , 1 0 9 ] [0,10^9] [0109]的数均未出现,所以为0)
N , M ( 操 作 次 数 ) ≤ 1 0 5 N,M(操作次数)≤10^5 NM105
时限:4s


分析:

很直观的带修改的莫队算法题(我居然没学过就猜中了)

因为只学过普通的莫队算法,而且还是16年准备省选的时候学的,早就忘的差不多了,于是又回去补了补,现在重新写一个总结,以免以后再忘。

莫队算法:

莫队算法被称为区间问题的神器,但这神器的限制其实也颇多:必须离线操作,不支持区间修改,不支持无法在 O ( 1 ) O(1) O(1)或近似的复杂度内,将区间 [ l , r ] [l,r] [l,r]的答案通过加入一个值/删去一个值以得到 [ l − 1 , r ] [l-1,r] [l1,r], [ l , r − 1 ] [l,r-1] [l,r1], [ l + 1 , r ] [l+1,r] [l+1,r], [ l , r + 1 ] [l,r+1] [l,r+1]的答案的算法。并且,其复杂度为 ( N + M ) ∗ N (N+M)*\sqrt N (N+M)N ,所以当 N ≥ 1 0 6 N≥10^6 N106时,莫队算法就有TLE的可能。

说了那么多缺点,那么接下来就该大吹特吹了:
其优点是代码简单,考场上实用性强,是许多区间问题中bug级的存在,令出题人焦头烂额(谁也不想自己琢磨了半天搞出来的区间题,被人用莫队水过去)

先说说不带修改的莫队算法:
首先,莫队算法是基于分块进行的,将原串拆分为 N \sqrt N N 个块,每个块的大小自然也是 N \sqrt N N 级的,现在我们将所有询问,按照其左端点所在的块排序,左端在相同块中的,按照右端点排序。

假设当前已经得到了 [ L i , R i ] [L_i,R_i] [Li,Ri]的答案,我们通过依次插入和删除的方式,来得到 [ L i + 1 , R i + 1 ] [L_{i+1},R_{i+1}] [Li+1,Ri+1]
这看似十分暴力的算法,实则是有严格的复杂度证明的。

我们从 L L L R R R的偏移量来计算其复杂度:
首先是 L L L的偏移量:因为我们总共需要处理M个询问,每两个相邻的询问之间,若 L L L在同一个块中,那么其偏移量为 N \sqrt N N ,若不在同一个块中,其偏移量为 2 N 2\sqrt N 2N ,所以 L L L的复杂度为 M N M\sqrt N MN

然后是 R R R的偏移量:其实只要稍加分析就会发现,对于同一个块中, R R R是升序排列的,所以每个块的 R R R的偏移量均为 N N N,总计 N \sqrt N N 个块,所以 R R R的偏移量为 N N N\sqrt N NN

所以,莫队算法的复杂度就可以得到了: ( M + N ) N (M+N)\sqrt N (M+N)N

模板题:BZOJ2038小Z的袜子

#include
#include
#include
#include
#define SF scanf
#define PF printf
#define MAXN 50010
using namespace std;
int s[MAXN],n,m;
long long ans;
int b[MAXN];
int pos[MAXN];
struct node{
    int id;
    long long a,b,l,r;
}a[MAXN];
long long gcd(long long x,long long y){
    if(x==0)
        return y;
    return gcd(y%x,x);
}
bool cmp(node x,node y){
    return pos[x.l]<pos[y.l]||(pos[x.l]==pos[y.l]&&x.r<y.r);
}
bool sort_by_id(node x,node y){
    return x.id<y.id;
}
void init(){
    SF("%d%d",&n,&m);
    int block=sqrt(n);
    for(int i=0;i<n;i++){
        SF("%d",&b[i]);
        pos[i]=i/block;
    }
    for(int i=0;i<m;i++){
        SF("%d%d",&a[i].l,&a[i].r);
        a[i].id=i;
    }
}
void update(int x,int r){
    ans-=s[b[x]]*s[b[x]];
    s[b[x]]+=r;
    ans+=s[b[x]]*s[b[x]];
}
void work(){
    for(int i=0,l=0,r=-1;i<m;i++){
        if(a[i].l==a[i].r){
            a[i].b=1;
            continue;
        }
        for(;r>a[i].r-1;r--)
            update(r,-1);
        for(;r<a[i].r-1;r++)
            update(r+1,1);
        for(;l>a[i].l-1;l--)
            update(l-1,1);
        for(;l<a[i].l-1;l++)
            update(l,-1);
        a[i].a=ans-(a[i].r-a[i].l+1);
        a[i].b=(a[i].r-a[i].l+1)*(a[i].r-a[i].l);//PF("(%d %I64d %I64d %d)",ans,a[i].a,a[i].b,i);
        long long k=gcd(a[i].a,a[i].b);
        //PF("{%d}\n",k);
        a[i].a/=k;
        a[i].b/=k;
    }
}
int main(){
    init();
    sort(a,a+m,cmp);
    work();
    sort(a,a+m,sort_by_id);
    for(int i=0;i<m;i++)
        PF("%lld/%lld\n",a[i].a,a[i].b);
}

带修改的莫队算法(仅支持单点修改)

其实带修改的莫队算法本质并没有改变:
唯一的区别在于:我们分的块的大小不能是 N 0.5 N^{0.5} N0.5了,每个块的大小均为 N 2 3 N^{\frac 2 3} N32
这样一来,总的块的数量为 N 1 3 N^{\frac 1 3} N31

T i T_i Ti为该询问时已经做的修改次数
我们只需要以 L L L所在的块为最优先, R R R所在的块为第二优先, T T T为第三优先来将询问排序,在依次插入和删除新元素时,同时也处理 T T T(同样是依次修改)即可:

while(l>q[i].l) Add(a[--l]);
while(r<q[i].r)  Add(a[++r]);
while(l<q[i].l) Delet(a[l++]);
while(r>q[i].r)  Delet(a[r--]);
while(now<q[i].x) work(++now,i);
while(now>q[i].x) dework(now--,i);

时间复杂度的证明与不修改是类似的。

对于 L L L R R R的偏移量,是与询问次数M是相关的,每两次相邻询问之间, L L L的偏移量在 N 2 3 N^{\frac 2 3} N32数量级, R R R N 1 3 N^{\frac 1 3} N31次询问有可能会偏移 N N N,其余的询问偏移量同样也是 N 2 3 N^{\frac 2 3} N32数量级的。所以 L L L R R R的复杂度均为 O ( N 5 3 ) O(N^{\frac 5 3}) O(N35)(假设M与N为同一数量级)

对于 T T T的偏移量,与不修改中的 R R R偏移量类似,同样是:前两个维度所有可能情况*N
刚才已经说明了,块的数量为 N 1 3 N^{\frac 1 3} N31个,所以 L , R L,R L,R分别有 N 1 3 N^{\frac 1 3} N31种情况,其总的可能情况根据乘法原理为 N 2 3 N^{\frac 2 3} N32种,所以 T T T的复杂度同样也为 O ( N 5 3 ) O(N^{\frac 5 3}) O(N35)

提供一个小技巧:因为计算机算 N 2 3 N^{\frac 2 3} N32不方便计算,所以通常我们都是手算出 N N N的最大情况下的 N 2 3 N^{\frac 2 3} N32,将其作为一个常数直接赋值给块的大小即可。

模板题CF940F(可能这道题当模板有些不友好,所以附了题解)
题解:其实就是一个小技巧,我们可以将序列中的数字离散化,这里的离散化很简单,因为不用考虑大小关系,只需考虑是否相同即可,所以用一个map存储就能实现。
离散化之后,所有可能出现的值的个数就缩小到了 1 0 5 10^5 105级别,那么我们可以用一个数组存储每个数出现的次数即可,再用一个数组存储每个数出现次数的出现次数(有点抽象,看看代码可能有帮助)。
很容易发现,我们的答案一定是小于1000,所以我们暴力算这个值都没问题。

#include
#include
#include
#include
#include
#define SF scanf
#define PF printf
#define MAXN 200010
using namespace std;
int n,m,b,qnum,cnum;
struct node{
    int l,r,x,id;
}q[MAXN];
struct change{
    int pos,val,rval;
}c[MAXN];
int col[MAXN],res,blo[MAXN],a[MAXN],a1[MAXN],maxi;
int ans1[MAXN],ans[MAXN],ans2[MAXN];
map<int,int> mp;
int cmp(node a,node b){
    if(blo[a.l]!=blo[b.l])
        return blo[a.l]<blo[b.l];
    if(blo[a.r]!=blo[b.r])
        return blo[a.r]<blo[b.r];
    return a.x<b.x;
}
void Add(int val){
    if(ans1[val]>0)
        ans2[ans1[val]]--;
    ans1[val]++;
    ans2[ans1[val]]++;
}
void Delet(int val){
    ans2[ans1[val]]--;
    ans1[val]--;
    if(ans1[val]>0)
        ans2[ans1[val]]++;
}
void dework(int now,int i){
    if(c[now].pos>=q[i].l&&c[now].pos<=q[i].r){
        Delet(c[now].val);
        Add(c[now].rval);
    }
    a[c[now].pos]=c[now].rval;
}
void work(int now,int i){
    if(c[now].pos>=q[i].l&&c[now].pos<=q[i].r){
        Delet(c[now].rval);
        Add(c[now].val);
    }
    a[c[now].pos]=c[now].val;
}
void Moqueue(){
    int l=1,r=1,now=0;
    Add(a[1]);
    int i;
    for(i=1;i<=qnum;i++){
        while(l>q[i].l)
            Add(a[--l]);
        while(r<q[i].r)
            Add(a[++r]);
        while(l<q[i].l)
            Delet(a[l++]);
        while(r>q[i].r)
            Delet(a[r--]);
        while(now<q[i].x)
            work(++now,i);
        while(now>q[i].x)
            dework(now--,i);
        for(int j=1;;j++)
            if(ans2[j]==0){
                ans[q[i].id]=j;
                break;
            }
    }
    for(int i=1;i<=qnum;i++)
        PF("%d\n",ans[i]);
}
int main(){
    SF("%d%d",&n,&m);
    b=2000;
    int cnt=0;
    for(int i=1;i<=n;i++){
        SF("%d",&a[i]);
        if(mp[a[i]]==0)
            mp[a[i]]=++cnt;
        a[i]=mp[a[i]];
        a1[i]=a[i];
        blo[i]=(i-1)/b+1;
    }
    int tag,x,y;
    for(int i=1;i<=m;i++){
        SF("%d%d%d",&tag,&x,&y);
        if(tag==1){
            q[++qnum].l=x;
            q[qnum].r=y;
            q[qnum].x=cnum;
            q[qnum].id=qnum;
        }
        else{
            c[++cnum].pos=x;
            if(mp[y]==0)
                mp[y]=++cnt;
            y=mp[y];
            c[cnum].val=y;
            c[cnum].rval=a1[x];
            a1[x]=y;
        }
    }
    sort(q+1,q+1+qnum,cmp);
    Moqueue();
}

树上莫队

//这年头,什么都流行上树,像什么母猪,HJB上树都不新鲜了,所以我们的莫队算法也要上树
其实树上莫队只是借助了DFS序列(括号序列,每个元素出现两次,设靠前的一次为 f i r [ x ] fir[x] fir[x],靠后的一次为 l a s [ i ] las[i] las[i])而已

总所周知,DFN是个神奇的工具,能够将树形结构压缩成一个一维的数组。
莫队算法作为区间问题的神器,不能直接在树上搞,所以就只能在DFN上进行操作

比较简单的是询问子树信息(DFN序),对于每次询问的点x,我们更改为:
L = f i r [ x ] , R = l a s [ i ] L=fir[x],R=las[i] L=fir[x],R=las[i]
所以我们就将问题转化为求DFN中 [ L , R ] [L,R] [L,R]这个区间了

比较恶心一点的是求路径信息(括号序列),对于每次询问的两个点 ( u , v ) (u,v) (u,v)
如果一点是另一点的祖先(设:u为v的祖先)
L = l a s [ v ] , R = l a s [ u ] L=las[v],R=las[u] L=las[v],R=las[u]
如果两点之间没有直接的祖先关系(设u在DFN序列中比v更靠前出现):
L = l a s [ u ] , R = f i r [ v ] L=las[u],R=fir[v] L=las[u],R=fir[v]
但这种情况很容易发现它们的lca没有出现在这个区间内,所以再加入lca即可。

当然还有带修改的树上莫队(笑),其做法就是:树上莫队+带修改莫队即可
不带修改模板题:COT2

#include
#include
#include
#include
#include
#include
#define SF scanf
#define PF printf
#define MAXN 100010
using namespace std;
vector<int> a[MAXN];
int fir[MAXN],bac[MAXN],col[MAXN];
int blo[MAXN],dfn[MAXN],cnt,sum[MAXN];
int fa[MAXN][20],deep[MAXN],ansx[MAXN],n,m,ans;
bool used[MAXN];
struct node{
    int l,r,id;
    bool p;
}q[MAXN];
map<int,int> mp;
void dfs(int x){
    dfn[++cnt]=x;
    fir[x]=cnt;
    deep[x]=deep[fa[x][0]]+1;
    for(int i=1;i<20;i++)
        fa[x][i]=fa[fa[x][i-1]][i-1];
    for(int i=0;i<a[x].size();i++){
        if(a[x][i]==fa[x][0])
            continue;
        fa[a[x][i]][0]=x;
        dfs(a[x][i]);
    }
    dfn[++cnt]=x;
    bac[x]=cnt;
}
int lca(int x,int y){
    if(deep[x]<deep[y])
        swap(x,y);
    for(int i=19;i>=0;i--)
        if(deep[fa[x][i]]>=deep[y])
            x=fa[x][i];
    if(x==y)
        return x;
    for(int i=19;i>=0;i--)
        if(fa[x][i]!=fa[y][i]){
            x=fa[x][i];
            y=fa[y][i];
        }
    return fa[x][0];
}
bool cmp(node a,node b){
    if(blo[a.l]!=blo[b.l])
        return blo[a.l]<blo[b.l];
    return a.r<b.r;
}
void Add(int x){
    if(used[x]==1){
        sum[col[x]]--;
        if(sum[col[x]]==0)
            ans--;
        used[x]=0;
    }
    else{
        sum[col[x]]++;
        if(sum[col[x]]==1)
            ans++;
        used[x]=1;
    }
}
void Moqueue(){
    int l=1,r=1;
    Add(dfn[1]);
    for(int i=1;i<=m;i++){
        while(l>q[i].l)
            Add(dfn[--l]);
        while(l<q[i].l)
            Add(dfn[l++]);
        while(r<q[i].r)
            Add(dfn[++r]);
        while(r>q[i].r)
            Add(dfn[r--]);
        int lcax=lca(dfn[l],dfn[r]);
        if(q[i].p)
            Add(lcax);
        ansx[q[i].id]=ans;
        if(q[i].p)
            Add(lcax);
    }
    for(int i=1;i<=m;i++)
        PF("%d\n",ansx[i]);
}
int main(){
    //freopen("data.in","r",stdin);
    //freopen("data.out","w",stdout);
    int x,y;
    SF("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        SF("%d",&col[i]);
        if(mp[col[i]]==0)
            mp[col[i]]=++cnt;
        col[i]=mp[col[i]];
    }
    for(int i=1;i<n;i++){
        SF("%d%d",&x,&y);
        a[x].push_back(y);
        a[y].push_back(x);
    }
    dfs(1);
    int b=sqrt(cnt);
    for(int i=1;i<=cnt;i++)
        blo[i]=i/b+1;
    for(int i=1;i<=m;i++){
        SF("%d%d",&q[i].l,&q[i].r);
        if(fir[q[i].l]>fir[q[i].r])
            swap(q[i].l,q[i].r);
        if(bac[q[i].l]>fir[q[i].r]){
            x=bac[q[i].r];
            y=bac[q[i].l];
            q[i].p=0;
        }
        else{
            x=bac[q[i].l];
            y=fir[q[i].r];
            q[i].p=1;
        }
        q[i].l=x;
        q[i].r=y;
        q[i].id=i;
    }
    sort(q+1,q+1+m,cmp);
    Moqueue();
}

带修改模板题:糖果公园

#include
#include
#include
#include
#include
#include
#define SF scanf
#define PF printf
#define MAXN 200010
using namespace std;
vector<int> a[MAXN];
int fir[MAXN],bac[MAXN],col[MAXN],col1[MAXN];
int blo[MAXN],dfn[MAXN],cnt;
int fa[MAXN][20],deep[MAXN],n,cols,m,cntc,cntq;
bool used[MAXN];
long long ans,w[MAXN],v[MAXN],sum[MAXN],ansx[MAXN];
struct node{
    int l,r,id,cntc;
    bool p;
}q[MAXN];
struct chan{
    int pos,val,rval;
}p[MAXN];
void dfs(int x){
    dfn[++cnt]=x;
    fir[x]=cnt;
    deep[x]=deep[fa[x][0]]+1;
    for(int i=1;i<20;i++)
        fa[x][i]=fa[fa[x][i-1]][i-1];
    for(int i=0;i<a[x].size();i++){
        if(a[x][i]==fa[x][0])
            continue;
        fa[a[x][i]][0]=x;
        dfs(a[x][i]);
    }
    dfn[++cnt]=x;
    bac[x]=cnt;
}
int lca(int x,int y){
    if(deep[x]<deep[y])
        swap(x,y);
    for(int i=19;i>=0;i--)
        if(deep[fa[x][i]]>=deep[y])
            x=fa[x][i];
    if(x==y)
        return x;
    for(int i=19;i>=0;i--)
        if(fa[x][i]!=fa[y][i]){
            x=fa[x][i];
            y=fa[y][i];
        }
    return fa[x][0];
}
bool cmp(node a,node b){
    if(blo[a.l]!=blo[b.l])
        return blo[a.l]<blo[b.l];
    if(blo[a.r]!=blo[b.r])
        return blo[a.r]<blo[b.r];
    return a.cntc<b.cntc;
}
void Add(int x){
    if(used[x]==1){
        ans-=w[sum[col[x]]]*v[col[x]];
        sum[col[x]]--;
        used[x]=0;
    }
    else{
        sum[col[x]]++;
        ans+=w[sum[col[x]]]*v[col[x]];
        used[x]=1;
    }
}
void work(int i,int j){
    if(used[p[i].pos]==1){
        Add(p[i].pos);
        col[p[i].pos]=p[i].val;
        Add(p[i].pos);
    }
    col[p[i].pos]=p[i].val;
}
void dework(int i,int j){
    if(used[p[i].pos]==1){
        Add(p[i].pos);
        col[p[i].pos]=p[i].rval;
        Add(p[i].pos);
    }
    col[p[i].pos]=p[i].rval;
}
void Moqueue(){
    int l=1,r=1,now=0;
    Add(dfn[1]);
    for(int i=1;i<=cntq;i++){
        while(l>q[i].l)
            Add(dfn[--l]);
        while(l<q[i].l)
            Add(dfn[l++]);
        while(r<q[i].r)
            Add(dfn[++r]);
        while(r>q[i].r)
            Add(dfn[r--]);
        while(now<q[i].cntc)
            work(++now,i);
        while(now>q[i].cntc)
            dework(now--,i);
        int lcax=lca(dfn[l],dfn[r]);
        if(q[i].p)
            Add(lcax);
        ansx[q[i].id]=ans;
        if(q[i].p)
            Add(lcax);
    }
    for(int i=1;i<=cntq;i++)
        PF("%lld\n",ansx[i]);
}
int main(){
    //freopen("data.in","r",stdin);
    //freopen("data.out","w",stdout);
    int x,y;
    SF("%d%d%d",&n,&cols,&m);
    for(int i=1;i<=cols;i++)
        SF("%d",&v[i]);
    for(int i=1;i<=n;i++)
        SF("%d",&w[i]);
    for(int i=1;i<n;i++){
        SF("%d%d",&x,&y);
        a[x].push_back(y);
        a[y].push_back(x);
    }
    for(int i=1;i<=n;i++){
        SF("%d",&col[i]);
        col1[i]=col[i];
    }
    dfs(1);
    int b=3000,tag;
    for(int i=1;i<=cnt;i++)
        blo[i]=i/b+1;
    for(int i=1;i<=m;i++){
        SF("%d",&tag);
        if(tag==1){
            cntq++;
            SF("%d%d",&q[cntq].l,&q[cntq].r);
            q[cntq].cntc=cntc;
            if(fir[q[cntq].l]>fir[q[cntq].r])
                swap(q[cntq].l,q[cntq].r);
            if(bac[q[cntq].l]>fir[q[cntq].r]){
                x=bac[q[cntq].r];
                y=bac[q[cntq].l];
                q[cntq].p=0;
            }
            else{
                x=bac[q[cntq].l];
                y=fir[q[cntq].r];
                q[cntq].p=1;
            }
            q[cntq].l=x;
            q[cntq].r=y;
            q[cntq].id=cntq;
        }
        else{
            cntc++;
            int pos,val;
            SF("%d%d",&pos,&val);
            p[cntc].pos=pos;
            p[cntc].val=val;
            p[cntc].rval=col1[pos];
            col1[pos]=val;
        }
    }
    sort(q+1,q+1+cntq,cmp);
    Moqueue();
}

你可能感兴趣的:(总结,莫队算法)