莫队

补一下莫队。

莫队

对于序列上的询问问题,如果 \([l,r]\) 的答案能 \(O(1)\) 扩展到 \([l-1,r],[l+1,r],[l,r-1],[l,r+1]\) ,那么我们就可以用莫队来以 \(O(n\sqrt n)\) 的复杂度离线解决。

实现实际上非常简单,离线所有询问然后排序,对序列分块,以 \(l\) 所在块为第一关键字, \(r\) 为第二关键字排序。

例题:小Z的袜子

这个概率可以直接变成数区间相同元素个数。我们知道如果有 \(n\) 只相同颜色袜子那么贡献就是 \(\frac {n(n-1)}2\) 。实际上莫队扫到每只袜子的时候加上前面所有袜子的数量(就是这只袜子能配多少对)就行了。

一个小优化:奇偶化排序。奇数块 \(r\) 从小到大,偶数块 \(r\) 从大到小。

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
int sq;
struct node{
	int l,r,id;
	bool operator<(const node& s)const{
		if(l/sq!=s.l/sq)return l<s.l;
		if((l/sq)&1)return r<s.r;
		return r>s.r;
	}
}q[50010];
int num[50010],a[50010],n,m,sum;
int ans1[50010],ans2[50010];
void add(int k){
	sum+=num[k];num[k]++;
}
void del(int k){
	num[k]--;sum-=num[k];
}
int gcd(int a,int b){
	if(b==0)return a;else return gcd(b,a%b);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&q[i].l,&q[i].r);
		q[i].id=i;
	}sq=sqrt(n);
	sort(q+1,q+m+1);
	for(int i=1,l=1,r=0;i<=m;i++){
		if(q[i].l==q[i].r){
			ans2[q[i].id]=1;continue;
		}
		while(l>q[i].l)add(a[--l]);
		while(r<q[i].r)add(a[++r]);
		while(l<q[i].l)del(a[l++]);
		while(r>q[i].r)del(a[r--]);
		ans1[q[i].id]=sum;
		ans2[q[i].id]=1ll*(r-l+1)*(r-l)/2;
	}
	for(int i=1;i<=m;i++){
		if(ans1[i]==0)printf("0/1\n");
		else{
			int d=gcd(ans1[i],ans2[i]);
			printf("%d/%d\n",ans1[i]/d,ans2[i]/d);
		}
	}
	return 0;
}

带修莫队

众所周知普通的莫队不支持修改。所以我们给它加上一个时间维。于是每次就有三个关键字可以加减。

实际上设莫队的维数是 \(k\) ,那么莫队的最坏复杂度是 \(O(n^{2-\frac 1k})\) 的。块长是 \(n^{1-\frac 1k}\) 的时候复杂度是最优的。当然某些毒瘤题里常数块长可能跑的更快。

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
int a[2000010],n,m,sq,ans[2000010];
int col[2000010],sum;
struct ques{
	int l,r,t,id;
	bool operator<(const ques& s)const{
		if(l/sq!=s.l/sq)return l<s.l;
		if(r/sq!=s.r/sq)return r<s.r;
		return t<s.t;
	}
}q[2000010];
struct node{
	int pos,num;
}rep[2000010];
inline void add(int x){
	if(col[x]==0)sum++;col[x]++;
}
inline void del(int x){
	col[x]--;if(col[x]==0)sum--;
}
inline void update(int l,int r,int t){
	if(l<=rep[t].pos&&rep[t].pos<=r){
		del(a[rep[t].pos]);add(rep[t].num);
	}swap(a[rep[t].pos],rep[t].num);
}
int main(){
	scanf("%d%d",&n,&m);
	sq=pow(n,0.666);int cnt=0,ti=0;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=m;i++){
		char ch[5];scanf("%s",ch);
		if(ch[0]=='Q'){
			cnt++;scanf("%d%d",&q[cnt].l,&q[cnt].r);
			q[cnt].t=ti;q[cnt].id=cnt;
		}
		else{
			ti++;scanf("%d%d",&rep[ti].pos,&rep[ti].num);
		}
	}sort(q+1,q+cnt+1);
	for(int i=1,l=1,r=0,t=0;i<=cnt;i++){
		while(l>q[i].l)add(a[--l]);
		while(r<q[i].r)add(a[++r]);
		while(l<q[i].l)del(a[l++]);
		while(r>q[i].r)del(a[r--]);
		while(t<q[i].t)update(l,r,++t);
		while(t>q[i].t)update(l,r,t--);
		ans[q[i].id]=sum;
	}
	for(int i=1;i<=cnt;i++)printf("%d\n",ans[i]);
	return 0;
}

回滚莫队

如果一个题看上去很可以莫队,但是删除不太好搞,就可以使用回滚莫队。

回滚莫队,即不删除(当然你也可以不加入是吧)莫队,它的大体步骤是:

  1. 对序列分块,对询问排序(不能奇偶化)。
  2. 按顺序处理询问:如果左端点所属块与上一个询问不同,将左端点设为块的右端点 \(+1\) ,右端点设为块的右端点(即初始的空区间)。
  3. 如果左右端点在同一块,那么直接暴力扫。
  4. 如果不在同一块,先扩展右端点,然后记录一下当前的状态,再扩展左端点,最后直接还原刚才记录的状态并还原左端点。

复杂度还是 \(O(n\sqrt n)\) 的。

例题:AT1219 歴史の研究

#include <algorithm>
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#define int long long
using namespace std;
int n,m,sq,sum,a[100010],lsh[100010],L[510],R[510];
int ans[100010],cnt[100010],belong[100010];
struct node{
    int l,r,id;
    bool operator<(const node &s)const{
        return belong[l]==belong[s.l]?r<s.r:l<s.l;
    }
}q[100010];
int ret[100010];
int solve(int l,int r){
    int ans=0;
    for(int i=l;i<=r;i++)ret[a[i]]++;
    for(int i=l;i<=r;i++)ans=max(ans,lsh[a[i]]*ret[a[i]]);
    for(int i=l;i<=r;i++)ret[a[i]]--;
    return ans;
}
void add(int x){
    cnt[a[x]]++;
    sum=max(sum,cnt[a[x]]*lsh[a[x]]);
}
void del(int x){
    cnt[a[x]]--;
}
signed main(){
    scanf("%lld%lld",&n,&m);sq=sqrt(n);
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);lsh[i]=a[i];
    }
    sort(lsh+1,lsh+n+1);
    int num=unique(lsh+1,lsh+n+1)-lsh-1;
    for(int i=1;i<=n;i++)a[i]=lower_bound(lsh+1,lsh+num+1,a[i])-lsh;
    for(int i=1;i<=m;i++){
        scanf("%lld%lld",&q[i].l,&q[i].r);q[i].id=i;
    }
    int bl=n/sq;
    for(int i=1;i<=bl;i++){
        L[i]=(i-1)*sq+1;R[i]=i*sq;
    }
    R[bl]=n;
    for(int i=1;i<=bl;i++){
        for(int j=L[i];j<=R[i];j++)belong[j]=i;
    }
    sort(q+1,q+m+1);
    for(int x=1,i=1,l=1,r=0;x<=bl;x++){
        memset(cnt,0,sizeof(cnt));
        l=R[x]+1;r=R[x];sum=0;
        while(belong[q[i].l]==x){
            if(belong[q[i].l]==belong[q[i].r])ans[q[i].id]=solve(q[i].l,q[i].r);
            //同一块内直接暴力扫
            else{
                while(q[i].r>r)add(++r);//扩展r
                int ret=sum;//记录
                while(l>q[i].l)add(--l);//扩展l
                ans[q[i].id]=sum;sum=ret;
                while(l<=R[x])del(l++);//更新答案并还原状态
            }
            i++;
        }
    }
    for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
}

二次离线莫队

普通莫队的复杂度保证在于扩展区间的复杂度很小。但是如果这个复杂度比较大那就寄了。

二次离线莫队通过把扩展区间的过程也离线下来来把复杂度从 \(n\sqrt mf(n)\) 变成 \(nf(n)+n\sqrt m\)。要求信息可减。

对于一次扩展(以扩展右端点为例,左端点是一样的过程):新计算的贡献是 \(r+1\)\([l,r]\) 的贡献,不妨记作 \(f(r+1,[l,r])\)。那么差分一下,就是 \(f(r+1,[1,r])-f(r+1,[1,l-1])\)。第一部分是一个前缀后边第一个点对前缀的贡献,可以 \(O(nf(n))\) 预处理得到。然后模拟莫队的移动端点过程,并把所有形如 \(f(r+1,[1,l-1])\) 的询问挂在 \(l-1\) 上,并在最后扫描线更新答案。为了方便处理,可以差分贡献最后加起来。

例题:P4887 【模板】莫队二次离线(第十四分块(前体))

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
#include <cmath>
using namespace std;
int n,sq,m,k,sum,a[100010];
int L[510],R[510],belong[100010];
long long ans[100010];
struct node{
    int l,r,id;
    bool operator<(const node&s)const{
        if(belong[l]==belong[s.l])return r<s.r;
        return l<s.l;
    }
}q[100010];
vector<int>v;
vector<node>ques[100010];
int cnt[1<<14],pre[100010];
int main(){
    scanf("%d%d%d",&n,&m,&k);sq=sqrt(n);
    if(k>14){
        while(m--)puts("0");return 0;
    }
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    for(int i=1;i<=m;i++)scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
    int bl=n/sq;
    for(int i=1;i<=bl;i++)L[i]=R[i-1]+1,R[i]=i*sq;R[bl]=n;
    for(int i=1;i<=bl;i++){
        for(int j=L[i];j<=R[i];j++)belong[j]=i;
    }
    sort(q+1,q+m+1);
    for(int i=0;i<(1<<14);i++)if(__builtin_popcount(i)==k)v.push_back(i);
    for(int i=1;i<=n;i++){
        for(int x:v)cnt[a[i]^x]++;
        pre[i]=cnt[a[i+1]];
    }
    for(int i=1,l=1,r=0;i<=m;i++){
        if(l>q[i].l)ques[r].push_back({q[i].l,l-1,q[i].id});
        while(l>q[i].l)l--,ans[q[i].id]-=pre[l-1];
        if(r<q[i].r)ques[l-1].push_back({r+1,q[i].r,-q[i].id});
        while(r<q[i].r)ans[q[i].id]+=pre[r],r++;
        if(l<q[i].l)ques[r].push_back({l,q[i].l-1,-q[i].id});
        while(l<q[i].l)ans[q[i].id]+=pre[l-1],l++;
        if(r>q[i].r)ques[l-1].push_back({q[i].r+1,r,q[i].id});
        while(r>q[i].r)r--,ans[q[i].id]-=pre[r];
    }
    memset(cnt,0,sizeof(cnt));
    for(int i=1;i<=n;i++){
        for(int x:v)cnt[a[i]^x]++;
        for(node q:ques[i]){
            for(int j=q.l;j<=q.r;j++){
                int tmp=cnt[a[j]];
                if(j<=i&&!k)tmp--;
                if(q.id<0)ans[-q.id]-=tmp;
                else ans[q.id]+=tmp;
            }
        }
    }
    for(int i=1;i<=m;i++)ans[q[i].id]+=ans[q[i-1].id];
    for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
    return 0;
}

树上莫队

把树拍到括号序上跑莫队。进出的时候都在序列末尾加上 \(x\),并记录每个点的进出时间 \(st_x,ed_x\)。一个区间中如果 \(x\) 出现两次那么是没有贡献的,可以开个 \(vis\) 数组存出现了多少次。然后直接跑莫队即可。对于把链变成区间,分类讨论一下:(假设 \(st_x<st_y\)

  1. 如果有祖先后代关系(设 \(x\)\(y\) 祖先),则区间为 \([st_x,st_y]\)
  2. 否则,区间为 \(ed_x,st_y\),但 \(\text{lca}(x,y)\) 并不在区间里,所以查询的时候要加上 \(\lca{x,y}\) 的贡献。

校内模拟赛的一份丑陋的代码:



/*数据结构很好玩?*/
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;
int n,m,sq;
struct node{
    int v,next;
}edge[1000010];
int t,head[500010],a[500010];
void add(int u,int v){
    edge[++t].v=v;edge[t].next=head[u];head[u]=t;
}
int num,dfn[1000010],st[500010],ed[500010];
void dfs(int x,int f){
    dfn[++num]=x;st[x]=num;
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=f){
            dfs(edge[i].v,x);
        }
    }
    dfn[++num]=x;ed[x]=num;
}
namespace LCA{
    int num,dfn[500010],size[500010],fa[500010],top[500010],son[500010],dep[500010];
    void dfs1(int x,int f){
        size[x]=1;fa[x]=f;dep[x]=dep[f]+1;
        for(int i=head[x];i;i=edge[i].next){
            if(edge[i].v!=f){
                dfs1(edge[i].v,x);
                if(size[son[x]]<size[edge[i].v])son[x]=edge[i].v;
            }
        }
    }
    void dfs2(int x,int f,int tp){
        dfn[x]=++num;top[x]=tp;
        if(son[x])dfs2(son[x],x,tp);
        for(int i=head[x];i;i=edge[i].next){
            if(edge[i].v!=f&&edge[i].v!=son[x])dfs2(edge[i].v,x,edge[i].v);
        }
    }
    void pre(){
        dfs1(1,0);dfs2(1,0,1);
    }
    int lca(int x,int y){
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]])swap(x,y);
            x=fa[top[x]];
        }
        if(dep[x]>dep[y])swap(x,y);
        return x;
    }
}
using LCA::lca;
struct Ques{
    int l,r,id;
    bool operator<(const Ques&s)const{
        if(l/sq==s.l/sq){
            if((l/sq)&1)return r>s.r;
            return r<s.r;
        }
        return l<s.l;
    }
}q[500010];
int ans[500010];
struct Blo{
    int sq,cnt[500010],l[1010],r[1010],belong[500010],size[1010];
    void build(){
        sq=sqrt(n);
        for(int i=1;i<=sq;i++){
            l[i]=r[i-1]+1;r[i]=l[i]+sq-1;
        }
        r[sq]=n;
        for(int i=1;i<=sq;i++){
            for(int j=l[i];j<=r[i];j++)belong[j]=i;
        }
    }
    void add(int x){
        cnt[x]++;
        if(cnt[x]==1)size[belong[x]]++;
    }
    void del(int x){
        cnt[x]--;
        if(cnt[x]==0)size[belong[x]]--;
    }
    int query(){
        int i;
        for(i=1;i<=sq;i++){
            if(size[i]!=r[i]-l[i]+1)break;
        }
        for(int j=l[i];j<=r[i];j++)if(!cnt[j])return j;
        return n+1;
    }
}blo;
bool vis[500010];
void update(int x){
    vis[x]=!vis[x];
    if(vis[x])blo.add(a[x]);
    else blo.del(a[x]);
}
int read(){
    int x=0;char ch=getchar();
    while(!isdigit(ch))ch=getchar();
    while(isdigit(ch))x=10*x+ch-'0',ch=getchar();
    return x;
}
void print(int x){
    if(x>=10)print(x/10);
    putchar(x%10+'0');
}
int main(){
    n=read();m=read();sq=2*n/sqrt(m);
    for(int i=1;i<=n;i++)a[i]=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        add(u,v);add(v,u);
    }
    dfs(1,0);LCA::pre();blo.build();
    for(int i=1;i<=m;i++){
        int u=read(),v=read();
        if(st[u]>st[v])swap(u,v);
        q[i]={lca(u,v)==u?st[u]:ed[u],st[v],i};
    }
    sort(q+1,q+m+1);
    for(int i=1,l=1,r=0;i<=m;i++){
        while(l>q[i].l)update(dfn[--l]);
        while(r<q[i].r)update(dfn[++r]);
        while(l<q[i].l)update(dfn[l++]);
        while(r>q[i].r)update(dfn[r--]);
        int lc=lca(dfn[q[i].l],dfn[q[i].r]);
        if(lc!=dfn[q[i].l]&&lc!=dfn[q[i].r]){
            update(lc);
            ans[q[i].id]=blo.query();
            update(lc);
        }
        else ans[q[i].id]=blo.query();
    }
    for(int i=1;i<=m;i++)print(ans[i]),putchar('\n');
    return 0;
}
posted @ 2022-09-28 15:09  gtm1514  阅读(28)  评论(0编辑  收藏  举报