莫队
特别需要注意更新顺序。
参考 这个博客。
普通莫队
询问离线处理,通过合理地安排询问顺序使时间复杂度降下来。
块长 \(\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\) 没有被算进去。
把这两部分贡献加进去即可。
易错:
- 两部分额外贡献都有计算的时候,如果两者颜色相同,要效果叠加,不能单独计算 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;
}
- 查询的时候,如果 x 的左括号在 y 的左括号的右边,要 swap 一下。
- 调整修改操作的时候应只在:修改在 L 和 R 区间,且 vis = 1 的情况下把答案也修改。如果 vis = 0,因为没有贡献,没有修正的必要。
点击查看代码
``` #include回滚莫队
和带修莫队的区别是只添不删或只删不添。
下面内容搬自 这个博客。
只加不减的回滚莫队
我们考虑一个区间问题,若这个问题在区间转移中,加点操作得以实现,但是删点操作无法有效的实现时,就可以使用如下的莫队算法:
\(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)\),并且,在回答询问的过程中我们只进行了加点操作,没有涉及删点操作,这样就完成了我们需要的操作。
举个例子帮助理解:(搬自 这个博客)

具体来分析吧。
对于 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
板子。借这道题提几个容易错的点。
- 发现当前块的询问处理完了以后,下一个询问不一定在下一个块,有可能在更远的块。
- 暴力扫的计数数组要和莫队维护的计数数组分开存。
点击查看代码
#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;
}

浙公网安备 33010602011771号