• 莫队算法学习笔记


    Part.1 引入

    当你遇到一个区间询问但是难以用线段树等 log 算法维护的时候怎么办?那就是——莫队!

    莫队这个东西能支持区间修改、区间查询的操作,但是这种算法要求离线。莫队有很多种,详细请看下文。

    Part.2 普通莫队

    我们先来看一道例题(P1972 的削弱版):

    给你一个长度为 n 的序列 am 次查询,询问区间 [l,r] 有多少个不同的数。

    数据范围:1n,m105,1ai105

    普通的暴力就是每次遍历这个区间,拿个桶记一下,每次需要清空桶。

    发现每次清空桶十分浪费,所以可以考虑从上一次的询问区间伸缩过来。就是记一个 tl,tr,初始为 tl=1,tr=0。每次把 tll 上靠,把 trr 上靠。代码如下:

    //add(x) 是加入下标为 x 的数,del(x) 是减去下标为 x 的数,这两个函数视情况而定
    while(tl>l) add(--tl);
    while(tradd(++tr);
    while(tldel(tl++);
    while(tr>r) del(tr--);
    

    当然这不是莫队,他是可以被卡的(大小区间交替询问,移动的量级就变成 nm)。

    莫队,就是通过离线后对询问左右端点排序,达到降低复杂度的目的。

    先讲做法,记一个块长 B=n,然后以 lB 为第一关键字,r 为第二关键字,从小到大排序。这样处理完所有询问的时间复杂度上界为 O(nn)

    为啥这样能保证时间复杂度呢?为了方便,我们定义点 i 在编号为 iB 的块内。不妨设 n,m 同阶。考虑两种情况:

    1. 上一个左端点和当前处理的左端点在一个块内:那么左端点最多移动 B 次,总共就只有 nB 次;询问左端点在一个块内的右端点是单调递增的,所以一个块内至多移动 n 次,而最多有 nB 个块,所以总询问次数是 nBn
    2. 上一个左端点和当前处理的左端点不在一个块内:左端点最多移动 2B 次,右端点也只移动 n 次,所以这部分的移动次数就只有 2BnB+nBn

    平均一下,当 Bn 时,时间复杂度就是 O(nn)

    给出上面例题的代码:

    #include
    using namespace std;
    const int N = 1e6+5;
    int n,m,a[N],cnt[N],res,ans[N],B;
    struct node{
        int l,r,id;
        inline void init(int x){cin>>l>>r,id = x;}
        inline bool friend operator < (node x,node y)//重载运算符,相当于写一个 cmp
        {
            if(x.l/B!=y.l/B) return x.lreturn x.rinline void add(int x)
    {
        x = a[x];
        cnt[x]++;
        if(cnt[x]==1) res++;
    }
    inline void del(int x)
    {
        x = a[x];
        cnt[x]--;
        if(!cnt[x]) res--;
    }
    signed main()
    {
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        cin>>n;
        B = sqrt(n);
        for(int i = 1;i<=n;i++)
            cin>>a[i];
        cin>>m;
        for(int i = 1;i<=m;i++)
            q[i].init(i);
        sort(q+1,q+m+1);
        int l = 1,r = 0;
        for(int i = 1;i<=m;i++)
        {
            while(l>q[i].l) add(--l);
            while(radd(++r);
            while(ldel(l++);
            while(r>q[i].r) del(r--);
            ans[q[i].id] = res;
        }
        for(int i = 1;i<=m;i++)
            cout<'\n';
        return 0;
    }
    

    当然,上述算法还能优化,比如奇偶排序(优化常数)、二次离线(去掉一只 log)。感兴趣的可以自己学习。

    Part.3 带修莫队

    普通莫队是不支持修改的,如果有修改操作的话,就可以请出带修莫队了!

    先给一道例题:P1903,其实就是在普通莫队的例题基础上加一个单点修改。

    其实就是给普通莫队加一个时间戳,即之前有多少个修改操作,排序时以其作为第三关键字。处理答案时就多记一个当前时间戳 t。每次把 t 移动到询问的时间戳,进行修改,并把在询问区间内的修改加入贡献。

    B 取到 n23 时,有最优复杂度 O(n53)。我太弱了,不会证明。

    贴上例题代码:

    #include 
    using namespace std;
    const int N = 133333+5,M = 1e6+5;
    int qsize;
    struct que{
    	int id,t,l,r;
    	inline friend bool operator < (que x,que y)
    	{
    		if(x.l/qsize!=y.l/qsize) return x.l/qsizeif(x.r/qsize!=y.r/qsize) return x.r/qsizereturn x.tstruct op{
    	int p,x;
    }o[N];
    int n,m,ans,mp[M],a[N],qcnt,ocnt,out[N];
    inline void add(int x)
    {
    	mp[x]++;
    	if(mp[x]==1) ans++;
    }
    inline void del(int x)
    {
    	mp[x]--;
    	if(!mp[x]) ans--;
    }
    signed main()
    {
    	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    	cin>>n>>m;
    	qsize = pow(n,2.0/3.0);
    	for(int i = 1;i<=n;i++)
    		cin>>a[i];
    	for(int i = 1,x,y;i<=m;i++)
    	{
    		char op;
    		cin>>op>>x>>y;
    		if(op=='Q') q[++qcnt] = {qcnt,ocnt,min(x,y),max(x,y)};
    		else o[++ocnt] = {x,y};
    	}
    	sort(q+1,q+qcnt+1);
    	int l = 1,r = 0,las = 0;
    	for(int i = 1;i<=qcnt;i++)
    	{
    		while(radd(a[++r]);
    		while(r>q[i].r) del(a[r--]);
    		while(l>q[i].l) add(a[--l]);
    		while(ldel(a[l++]);
    		while(lasif(o[las].p>=l&&o[las].p<=r) del(a[o[las].p]),add(o[las].x);
    			swap(a[o[las].p],o[las].x);
    		}
    		while(las>q[i].t)
    		{
    			if(o[las].p>=l&&o[las].p<=r) del(a[o[las].p]),add(o[las].x);
    			swap(a[o[las].p],o[las].x);
    			las--;
    		}
    		out[q[i].id] = ans;
    	}
    	for(int i = 1;i<=qcnt;i++)
    		cout<'\n';
    	return 0;
    }
    

    Part.4 回滚莫队

    回滚莫队解决的问题就是加入一个数很好维护,但是删除这个数不好维护(比如区间最值之类的)。其思想就是每次右端点慢慢加,左端点到目标点时计算答案再回到原来的点。

    仍然甩出一道例题:SP20644。让你统计区间中和为零的区间最大长度。

    先把问题转化成前缀和,相当于在问你区间中前缀和相同的地方最大的长度,然后就变成了P5906

    我们还是按照普通莫队的方式排序。回滚莫队由以下几部分组成:

    1. 左右端点在一个块内,直接暴力做;
    2. 左端点的块和上一个的不同,设这个块的右端点为 rt,那么 nowl 就要移动到 rt+1nowr 就要移动到 rt,并把当前答案清零;
    3. nowr 移动到当前询问的右端点,一边移动一边计算答案;
    4. nowl 移动到当前询问的左端点,注意移动完之后需要回到原来的位置,所以记录原来的答案以便复原,其余的正常计算贡献;
    5. nowl 移动回去,我们只需要把这段区间的贡献消掉就行,这是难点。然后把答案还原。

    需要注意的是,回滚莫队不支持奇偶排序

    回到这道例题,考虑如何消掉贡献。我们计算答案的时候维护一个 mxi 表示 i 最先出在那个位置,而 mxi 小于原来 nowi​ 的就会消掉贡献。

    放代码:

    #include 
    using namespace std;
    const int N = 5e4+5;
    int n,m,a[N],b[N],blk,lt[N],rt[N];
    struct node{
    	int l,r,id;
    	inline void init(int x){cin>>l>>r,l--,id = x;}
    	inline friend bool operator < (node x,node y)
    	{
    		if(b[x.l]!=b[y.l]) return x.lreturn x.rint mn[N<<1],mx[N<<1],ans[N];
    signed main()
    {
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        cin>>n>>m;
    	blk = sqrt(n);
    	a[0] = n,b[0] = 1;
    	for(int i = 1;i<=n;i++)
    		cin>>a[i],b[i] = (i-1)/blk+1,a[i]+=a[i-1];
    	for(int i = 1;i<=b[n];i++)
    		rt[i] = i*blk;
    	rt[b[n]] = n;
    	for(int i = 1;i<=m;i++)
    		q[i].init(i);
    	sort(q+1,q+m+1);
    	int l = 0,r = 0,las = 0,tmp = 0;
    	for(int i = 1;i<=m;i++)
    	{
    		if(b[q[i].l]==b[q[i].r])
    		{
    			for(int j = q[i].l;j<=q[i].r;j++)
    				mx[a[j]] = 0;
    			tmp = 0;
    			for(int j = q[i].r;j>=q[i].l;j--)
    				if(!mx[a[j]]) mx[a[j]] = j;
    				else tmp = max(tmp,mx[a[j]]-j);
    			ans[q[i].id] = tmp;
    			for(int j = q[i].l;j<=q[i].r;j++)
    				mx[a[j]] = 0;
    			continue;
    		}
    		if(b[q[i].l]!=las)
    		{
    			while(l1) mx[a[l]] = mn[a[l]] = 0,l++;
    			while(r>rt[b[q[i].l]]) mx[a[r]] = mn[a[r]] = 0,r--;
    			r = l-1;
    			tmp = 0,las = b[q[i].l];
    		}
    		while(rif(!mn[a[r]]) mn[a[r]] = mx[a[r]] = r;
    			else tmp = max(tmp,r-mn[a[r]]),mx[a[r]] = r;
    		}
    		int _l = l,res = tmp;
    		while(_l>q[i].l)
    		{
    			_l--; 
    			if(!mx[a[_l]]) mx[a[_l]] = _l;
    			else res = max(res,mx[a[_l]]-_l); 
    		}
    		ans[q[i].id] = res;
    		while(_lif(mx[a[_l]]==_l) mx[a[_l]] = 0;
    			_l++;
    		}
    	}
    	for(int i = 1;i<=m;i++)
    	    cout<'\n';
    	return 0;
    }
    

    另外推荐一道回滚莫队好题:AT_joisc2014_c

    Part.5 树上莫队

    还是先给一道例题:SP10707

    我们考虑对树进行 DFS,求出其欧拉序。

    给个例子:

    graph

    这颗树的欧拉序为 1,2,4,4,2,3,5,7,7,8,8,5,6,6,3,1。我们记节点 i 第一次出现的位置为 sti,第二次出现的位置为 edi

    考虑询问 uv 这条路径,不妨设 stu<stv,分两种情况讨论:

    1. vu 的子树中,我们只需要去掉 v 的子树,询问区间就是 stustv
    2. 否则,我们需要去掉 u,v 的子树,那么询问 edustv 即可。但是发现走到 stv 时还没有退出 lca(u,v) 的子树,所以还要单独算上 lca(u,v)

    其他的和普通莫队都是一样的,但注意要开两倍空间!

    上代码:

    #include 
    using namespace std;
    const int N = 1e5+5;
    int n,m,idx,ans[N],a[N],b[N],tt,cnt[N],res,st[N],ed[N],pre[N],son[N],dep[N],top[N],sz[N],f[N],qsize;
    vector<int> g[N];
    bool vis[N];
    void dfs1(int u,int fa)
    {
    	f[u] = fa,dep[u] = dep[fa]+1,sz[u] = 1,st[u] = ++idx,pre[idx] = u;
    	for(auto v:g[u])
    	{
    		if(v==fa) continue;
    		dfs1(v,u);
    		sz[u]+=sz[v];
    		if(sz[v]>sz[son[u]]) son[u] = v;
    	}
    	ed[u] = ++idx,pre[idx] = u;
    }
    void dfs2(int u,int tp)
    {
    	top[u] = tp;
    	if(!son[u]) return;
    	dfs2(son[u],tp);
    	for(auto v:g[u])
    	{
    		if(v==f[u]||v==son[u]) continue;
    		dfs2(v,v);
    	}
    }
    inline int Lca(int x,int y)
    {
    	while(top[x]!=top[y])
    	{
    		if(dep[top[x]]swap(x,y);
    		x = f[top[x]];
    	}
    	if(dep[x]>dep[y]) swap(x,y);
    	return x;
    }
    struct node{
    	int l,r,lca,id;
    	inline void init(int x)
    	{
    		id = x;
    		int u,v;
    		cin>>u>>v;
    		if(st[u]>st[v]) swap(u,v);
    		lca = Lca(u,v);
    		if(lca==u) l = st[u],r = st[v],lca = 0;
    		else l = ed[u],r = st[v];
    	}
    	inline friend bool operator < (node x,node y)
    	{
    		if(x.l/qsize==y.l/qsize) return x.rreturn x.linline void add(int x)
    {
    	cnt[x]++;
    	if(cnt[x]==1) res++;
    }
    inline void del(int x)
    {
    	cnt[x]--;
    	if(cnt[x]==0) res--;
    }
    inline void work(int x)
    {
    	x = pre[x];
    	vis[x]^=1;
    	if(vis[x]) add(a[x]);
    	else del(a[x]);
    }
    signed main()
    {
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        cin>>n>>m;
    	for(int i = 1;i<=n;i++)
    	    cin>>a[i],b[++tt] = a[i];
    	sort(b+1,b+tt+1),tt = unique(b+1,b+tt+1)-b-1;
    	for(int i = 1;i<=n;i++)
    		a[i] = lower_bound(b+1,b+tt+1,a[i])-b;
    	for(int i = 1,u,v;i>u>>v,g[u].push_back(v),g[v].push_back(u);
    	dfs1(1,0),dfs2(1,1);
    	for(int i = 1;i<=m;i++)
    		q[i].init(i);
    	qsize = sqrt(n);
    	sort(q+1,q+m+1);
    	int l = 1,r = 0;
    	for(int i = 1;i<=m;i++)
    	{
    		while(l>q[i].l) work(--l);
    		while(rwork(++r);
    		while(lwork(l++);
    		while(r>q[i].r) work(r--);
    		if(q[i].lca) work(st[q[i].lca]);
    		ans[q[i].id] = res;
    		if(q[i].lca) work(st[q[i].lca]);
    	}
    	for(int i = 1;i<=m;i++)
    	    cout<'\n';
    	return 0;
    }
    

    Part.6 总结

    莫队是个非常好的数据结构,建议深度学习!

    码字不易,给个赞吧~

    THE END

  • 相关阅读:
    stm32f334timer15-17
    【国产MCU】-CH32V307-独立看门狗(IWDG)
    三类6种地图可视化软件测评,最好用的工具居然是它
    Java自学(二)
    springboot 发布tomcat(war包)
    计算机网络第四层 运输层
    看看GPT-4V是怎么开车的,必须围观,大模型真的大有作为 | 万字长文
    【MFC】 前言(1)
    分享计算机msvcp100.dll,丢失或找不到的7个解决方法
    记第一次与世界的交流
  • 原文地址:https://www.cnblogs.com/pyy51316/p/18105710