朴素的莫队算法,它的核心理论个人认为是 分块 + 双指针 + 贪心思想,一般都是用来解决线段树所不能解决的区间问题(例如不区间众数等不满足区间加法的问题)。
一般题型像这样:
给出一个 n 长的序列,然后让你维护这个序列的特定值,再给你 q 个询问,求解这 q 个询问,这些询问的形式就是给你一个区间l,r。
一个简单的思路我们很容易想到,我们对这些询问区间进行排序(按 l 为第一关键字,r 为第二关键字),然后先算出第一个询问区间,再在接下来的区间中递增微调双指针l , r,对应的修改第一次求出来的值,按照这种方法依次求得多个区间的值。这样按理来说是可以的,但是当多个区间的 l 值相差不大,而相邻两个区间的 r 相差大这就造成了复杂度与n方基本无差的情况
为了解决这个问题我们将序列分块,也就是说将 l 邻近的值按 r 排序,这样就可以解决邻近块 r 的跨度较大的问题了,一般我们是划分出块,那么同样的在每一块里也就有
个元素,这样一来我们每一块微调的 l 指针的偏移量为
,而排序后的r 的偏移量也就很容易想到最多为 n 了,那么总复杂度也就为 o(n *
+ m *
) = o(max(n *
,m *
)).(n 为总长,m为询问数)
po:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int l = 0 , r = 1;
for(int i = 0 ; i < m ; ++ i)
{
while(l < a[i].l) upd(a[i],-1),l++;
while(l > a[i].l) upd(a[i],1) l--;
while(r < a[i].r) upd(a[i],1)r++;
while(r > a[i].r) upd(a[i],-1) r--;/*微调左右区间指针,
并且更新当前段的保留信息,即得到当前区间*/
/*
*....根据题意和当前段的信息更新答案
*/
}
}
持久化莫队:
如名,朴素化的莫队只支持纯粹的离线化解题,若我们有时需要单点修改值的时候,莫队算法该如何使用呢?
给询问的区间节点多加一个维度,表示它的修改时间,那么对于一个区间的排序我们就以它作为第三关键字,起的作用与前两关键字类同,对于修改时间,若当前询问区间的修改时间大于游动的时间指针,那么我们就把已经修改过的值一一修改回去,反之则全部修改为对应值
po:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int l = 0 , r = 1,time = 0;
for(int i = 0 ; i < m ; ++ i)
{
while(time < a[i].time) change(++time,1);
while(time > a[i].time) change(--time,0);/*将当前时间戳游动到合适的位置,
并选择是改回原来的样子还是修改成待修改的样子*/
while(l < a[i].l) upd(a[i],-1),l++;
while(l > a[i].l) upd(a[i],1) l--;
while(r < a[i].r) upd(a[i],1)r++;
while(r > a[i].r) upd(a[i],-1) r--;/*微调左右区间指针,并且更新当前段的保留信息,即得到当前区间*/
/*
*....根据题意和当前段的信息更新答案
*/
}
}
回滚莫队:
有时候莫队这种为当前区间增值更新或者减值更新时,很难满足某些题目的要求,比如区间最值。
为了解决这个问题,有人就提出了回滚的这种思想,对于在一块的 l和r我们可以直接暴力计算,不在同一块的l,r那就按 l 所在的块处理这个块的所有询问区间,然后让 r 一直往后扩大并保留当前 l 的状态,在处理下一个区间的时候由上一个保留的状态还原当前区间状态再回到起点(这个块的右端点)重新跑 l。当前块处理结束后,进入下一个块,清空莫队,初始化对应 l , r再次回滚。这个是对应与难减易增的,还有一种难增易减,是一种逆向思维,使 r 始终在最右边,然后回滚 r。