莫队

特别需要注意更新顺序。

参考 这个博客

普通莫队

询问离线处理,通过合理地安排询问顺序使时间复杂度降下来。

块长 \(\frac{n}{\sqrt{m}}\)\(n\) 为序列长,\(m\) 为询问数。

奇偶排序:左端点所在块不同时,按照左端点所在块排序。否则若左端点所在块为奇数块,则按照右端点升序排序;若左端点所在块为偶数块,则按照右端点降序排序。

bool cmp(dat x,dat y){
	if(bl[x.l] == bl[y.l]){
		if(bl[x.l] & 1)return x.r < y.r;
		else return x.r > y.r;
	}
	return bl[x.l] < bl[y.l];
}

时间复杂度为 \(O(n\sqrt{m})\)。据说可以跑 \(n=1e5,m=1e6\)

P4462 [CQOI2018] 异或序列

这题可以检验是否真的理解莫队的更新机制了。

莫队套分块

莫队可以离线处理询问。如果用莫队还原完当前询问的状态时不能快速处理询问,则可以试试分块。

P4396 [AHOI2013] 作业

问题在锁定询问区间后怎么查询值域。每次修改的时候调用树状数组太耗时间了,因为修改还要乘上莫队移指针的复杂度。考虑快速修改,查询可以耗点时间。分块在修改上具有很好的复杂度,单次查询复杂度为 \(O(\sqrt{n})\)。这是可以接受的。

P3730 曼哈顿交易

把热度的人数丢到分块里,类似平衡树查询第 k 大的方法跑一遍。

带修莫队

用于修改不独立的问题。依旧离线查询。

对询问进行神秘排序,记录询问前最后一次修改的编号。每次询问到一个地方,如果少修改了就多修改一点,如果多修改了就修正回去。然后调整双指针。修改双指针和调整修改,两者没有一定的先后顺序。

块长取 \(n^{\frac{2}{3}}\),或 \(\sqrt[3]{nt}\)。不会证。时间复杂度 \(O(n^{5/3})\)\(1e5\) 的数据 2s 可过,时限再紧可能要卡卡。

神秘排序:第一关键字为左端点所在块编号,第二关键字为右端点所在块编号,第三关键字为询问前修改了几次。

bool cmpv(dat x,dat y){
	if(x.l / nq != y.l / nq)return x.l < y.l;
	if(x.r / nq != y.r / nq)return x.r < y.r;
	return x.t < y.t;
}

修改完之后要把原修改数据取逆。比如 P1903 【模板】带修莫队 / [国家集训队] 数颜色 / 维护队列 的操作是 swap 颜色。为什么是swap参考这个

树上莫队

本质上是把树拍成序列。常用括号序,即进节点的时候记左括号,出节点的时候记右括号。

P4074 [WC2013] 糖果公园

带修莫队拍到树上。

先求括号序,然后询问拍到括号序(起点和终点都是左括号)。算进左括号的时候记贡献(vis: \(0 \to 1\)),算进右括号的时候把左括号的贡献消掉(vis: \(1 \to 0\)),这样可以忽略无关子树的贡献。大部分贡献都算在括号序内了,除了:

  • lca 不是起点的,起点没有被算进去,因为走完起点的子树把起点和其子树一起忽略掉了。
  • lca 不是起点也不是终点的,lca 节点没有被算进去,例如:12443321,询问 \(4 \to 3\),查询 \([3,5]\),然而 \(2\) 没有被算进去。

把这两部分贡献加进去即可。

易错:

  1. 两部分额外贡献都有计算的时候,如果两者颜色相同,要效果叠加,不能单独计算 cnt(cnt 有可能加了两次)。算完了记得修回来,因为这不是莫队维护的。
点击查看代码

int query(int x,int y){//错误
	int res = ans;
	int k = lca(x,y);
    if(k != x){
		res += v[c[k]] * w[cnt[c[k]] + 1];
		if(k != y)res += v[c[x]] * w[cnt[c[x]] + 1];
	}
	return res;
}

int query(int x,int y){//正确
	int res;
	int k = lca(x,y);
	if(k != x){
		add(c[x]);
		if(k != y)add(c[k]);
	}
	res = ans;
	if(k != x){
		del(c[x]);
		if(k != y)del(c[k]);
	}
	return res;
}
  1. 查询的时候,如果 x 的左括号在 y 的左括号的右边,要 swap 一下。
  2. 调整修改操作的时候应只在:修改在 L 和 R 区间,且 vis = 1 的情况下把答案也修改。如果 vis = 0,因为没有贡献,没有修正的必要。
点击查看代码 ``` #include #define int long long using namespace std; const int N = 100010; const int M = 1000010; int n,m,Q,hed[N],to[2 * N],nxt[2 * N],tote,v[M],w[M],b[2 * N],totb,p[N],dep[N],sz[N],hson[N],tp[N]; int faa[N],cnt[M],ans,vis[N],nq,L,R,t,totq,totr,c[N],ed[N],yid[2 * N]; void addedge(int x,int y){ tote++,nxt[tote] = hed[x],to[tote] = y,hed[x] = tote; } struct dat{ int x,y,l,r,id,t,ans; }q[N]; struct Dat{ int id,l,r,y; }r[N]; int read(){ int s = 0;char c = getchar();int f = 1; for(; c < '0' || c > '9'; c = getchar())(c == '-') && (f = -1); for(; c >= '0' && c <= '9'; c = getchar())s = (s << 1) + (s << 3) + (c ^ 48); return s * f; } void dfs1(int x,int fa){ b[++totb] = c[x],p[x] = totb,yid[totb] = x,sz[x] = 1,dep[x] = dep[fa] + 1; for(int i = hed[x]; i; i = nxt[i]){ int y = to[i]; if(fa == y)continue; dfs1(y,x); faa[y] = x,sz[x] += sz[y]; if(sz[hson[x]] < sz[y])hson[x] = y; } b[++totb] = c[x],ed[x] = totb,yid[totb] = x; } void dfs2(int x,int rt){ tp[x] = rt; if(hson[x])dfs2(hson[x],rt); for(int i = hed[x]; i; i = nxt[i]){ int y = to[i]; if(y == faa[x] || y == hson[x])continue; dfs2(y,y); } } int lca(int x,int y){ while(tp[x] != tp[y]){ if(dep[tp[x]] < dep[tp[y]])swap(x,y); x = faa[tp[x]]; } if(dep[x] < dep[y])swap(x,y); return y; } bool cmpv(dat x,dat y){ if(x.l / nq != y.l / nq)return x.l < y.l; if(x.r / nq != y.r / nq)return x.r < y.r; return x.t < y.t; } bool cmpid(dat x,dat y){return x.id < y.id;} void add(int x){cnt[x]++,ans += v[x] * w[cnt[x]];} void del(int x){ans -= v[x] * w[cnt[x]],cnt[x]--;} void upd(int x){ if(vis[yid[x]])del(b[x]); else add(b[x]); vis[yid[x]] ^= 1; } void modify(int k){ if(r[k].l >= L && r[k].l <= R){ if(vis[yid[r[k].l]])del(b[r[k].l]),add(r[k].y); } if(r[k].r >= L && r[k].r <= R){ if(vis[yid[r[k].r]])del(b[r[k].r]),add(r[k].y); } c[r[k].id] = b[r[k].r] = r[k].y,swap(b[r[k].l],r[k].y); } int query(int x,int y){ int res; int k = lca(x,y); if(k != x){ add(c[x]); if(k != y)add(c[k]); } res = ans; if(k != x){ del(c[x]); if(k != y)del(c[k]); } return res; } signed main(){ n = read(),m = read(),Q = read(); nq = pow(2 * n,2.0 / 3.0); for(int i = 1; i <= m; i++)v[i] = read(); for(int i = 1; i <= n; i++)w[i] = read(); for(int i = 1,x,y; i < n; i++)x = read(),y = read(),addedge(x,y),addedge(y,x); for(int i = 1; i <= n; i++)c[i] = read(); dfs1(1,0),dfs2(1,1); for(int i = 1,x,y,tp; i <= Q; i++){ tp = read(),x = read(),y = read(); if(tp == 1){ if(p[x] > p[y])swap(x,y); q[++totq] = {x,y,p[x],p[y],i,totr}; }else r[++totr] = {x,p[x],ed[x],y}; } sort(q + 1,q + totq + 1,cmpv); L = 1,R = 0,t = 0; for(int i = 1; i <= totq; i++){ while(L < q[i].l)upd(L++); while(L > q[i].l)upd(--L); while(R < q[i].r)upd(++R); while(R > q[i].r)upd(R--); while(t < q[i].t)modify(++t); while(t > q[i].t)modify(t--); q[i].ans = query(q[i].x,q[i].y); } sort(q + 1,q + totq + 1,cmpid); for(int i = 1; i <= totq; i++)printf("%lld\n",q[i].ans); return 0; } ```

回滚莫队

和带修莫队的区别是只添不删或只删不添。

下面内容搬自 这个博客

只加不减的回滚莫队

我们考虑一个区间问题,若这个问题在区间转移中,加点操作得以实现,但是删点操作无法有效的实现时,就可以使用如下的莫队算法:

\(1.\) 对原序列进行分块,并对询问按照如下的方式排序:以左端点所在的块升序为第一关键字,以右端点升序为第二关键字

\(2.\) 对于处理所有左端点在块\(T\)内的询问,我们先将莫队区间左端点初始化为\(R[T]+1\),右端点初始化为\(R[T]\),这是一个空区间

\(3.\) 对于左右端点在同一个块中的询问,我们直接暴力扫描回答即可。

\(4.\) 对于左右端点不在同一个块中的所有询问,由于其右端点升序,我们对右端点只做加点操作,总共最多加点\(n\)次

\(5.\) 对于左右端点不在同一个块中的所有询问,其左端点是可能乱序的,我们每一次从\(R[T]+1\)的位置出发,只做加点操作,到达询问位置即可,每一个询问最多加\(\sqrt n\)次。回答完询问后,我们撤销本次移动左端点的所有改动,使左端点回到\(R[T]+1\)的位置

\(6.\) 按照相同的方式处理下一块

根据其操作的过程可知,回滚莫队的时间复杂度仍然为\(O(n\sqrt n)\),并且,在回答询问的过程中我们只进行了加点操作,没有涉及删点操作,这样就完成了我们需要的操作。

举个例子帮助理解:(搬自 这个博客

image

具体来分析吧。

对于 1 号询问,由于就是在块内,所以暴力处理。

对于 2 号询问,莫队区间先是右端点从 2 扩展到 3,然后左端点扩展到 1,记录答案,左端点重新回到 3。

对于 3 号询问,莫队区间先是右端点从 3 扩展到 5,然后左端点从 3 来到 1,记录答案,左端点重新回到 3。

对于 4 号询问,莫队区间先是右端点从 5 扩展到 8,然后左端点从 3 来到 0,记录答案,左端点重新回到 3。

对于 5 号询问,莫队区间先是右端点从 8 扩展到 10,然后左端点从 3 来到 2,记录答案,左端点重新回到 3。

只减不加的回滚莫队

和上一种典型的回滚莫队类似,我们还可以实现只有删点操作没有加点操作的回滚莫队,当然,这样的前提是我们可以正确的先将整个序列加入莫队中,那么算法流程如下:

\(1.\) 对原序列进行分块,并对询问按照如下的方式排序:以左端点所在的块升序为第一关键字,以右端点降序序为第二关键字

\(2.\) 对于处理所有左端点在块\(T\)内的询问,我们先将莫队区间左端点初始化为\(L[T]\),右端点初始化为\(n\),这是一个大区间

\(3.\) 对于左右端点在同一个块中的询问,我们直接暴力扫描回答即可。

\(4.\) 对于左右端点不在同一个块中的所有询问,由于其右端点降序,从\(n\)的位置开始,我们对右端点只做删点操作,总共最多删点\(n\)次

\(5.\) 对于左右端点不在同一个块中的所有询问,其左端点是可能乱序的,我们每一次从\(L[T]\)的位置出发,只做删点操作,到达询问位置即可,每一个询问最多加\(\sqrt n\)次。回答完询问后,我们撤销本次移动左端点的所有改动,使左端点回到\(L[T]\)的位置

\(6.\) 按照相同的方式处理下一块

同样地,回滚莫队的时间复杂度还是\(O(n\sqrt n)\),并且我们只使用了删点操作,只有在一开始时将整个序列加入到莫队中,这样就完成了我们需要的操作。

P14420 [JOISC 2014] 历史的研究 / Historical Research

板子。借这道题提几个容易错的点。

  1. 发现当前块的询问处理完了以后,下一个询问不一定在下一个块,有可能在更远的块。
  2. 暴力扫的计数数组要和莫队维护的计数数组分开存。
点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 110010;
int n,Q,cntt[N],v[N],b[N],nq,bll[N],blr[N],totb,ans,cnt[N];
struct dat{
	int l,r,id,ans;
}q[N];
bool cmpv(dat x,dat y){
	if(x.l / nq != y.l / nq)return x.l < y.l;
	return x.r < y.r;
}
bool cmpid(dat x,dat y){return x.id < y.id;}
void getid(){
	while(++totb){
		bll[totb] = blr[totb - 1] + 1;
		if(blr[totb - 1] + nq >= 110000){blr[totb] = 110000;break;}
		blr[totb] = blr[totb - 1] + nq;
	}
}
int solve(int x){
	int res = 0;
	for(int i = q[x].l; i <= q[x].r; i++)cntt[b[i]]++,res = max(res,cntt[b[i]] * v[b[i]]);
	for(int i = q[x].l; i <= q[x].r; i++)cntt[b[i]]--;
	return res;
}
void add(int x){cnt[b[x]]++,ans = max(ans,cnt[b[x]] * v[b[x]]);}
void del(int x){cnt[b[x]]--;}
signed main(){
	scanf("%lld%lld",&n,&Q);
	nq = n / sqrt(Q);
	getid();
	for(int i = 1; i <= n; i++)scanf("%lld",&b[i]),v[i] = b[i];
	sort(v + 1,v + n + 1);
	int m = unique(v + 1,v + n + 1) - v - 1;
	for(int i = 1; i <= n; i++)b[i] = lower_bound(v + 1,v + m + 1,b[i]) - v;
	for(int i = 1; i <= Q; i++)scanf("%lld%lld",&q[i].l,&q[i].r),q[i].id = i;
	sort(q + 1,q + Q + 1,cmpv);
	int L = 1,R = 0;
	for(int i = 1,j = 0; i <= Q; i++){
		if(q[i].l >= blr[j]){
			while(q[i].l >= blr[j])j++;
			L = blr[j] + 1,R = blr[j];ans = 0;
			for(int k = 1; k <= m; k++)cnt[k] = 0;
		}
		//cout << q[i].id << " " << j << " " << (q[i].r <= blr[j] ? 1 : 0) << endl;
		if(q[i].r <= blr[j]){q[i].ans = solve(i);continue;}
		while(R < q[i].r)add(++R);
		int res = ans;
		while(L > q[i].l)add(--L);
		q[i].ans = ans;
		while(L < blr[j] + 1)del(L++);
		ans = res;
	}
	sort(q + 1,q + Q + 1,cmpid);
	for(int i = 1; i <= Q; i++)printf("%lld\n",q[i].ans);
	return 0;
}
posted @ 2026-05-13 19:25  Jenny_yu  阅读(8)  评论(0)    收藏  举报