上一篇讲了带修莫队的具体内容,在我看来带修莫队的思路比较清晰,无非是在跑区间的过程中加入元素的修改,只需要考虑查询与修改的时间相对性就可以了。这次要分享的是另一种莫队算法的拓展——回滚&不删除莫队。
还是先贴一道经典例题
https://www.luogu.com.cn/problem/AT_joisc2014_chttps://www.luogu.com.cn/problem/AT_joisc2014_c
题目描述
IOI 国历史研究的第一人——JOI 教授,最近获得了一份被认为是古代 IOI 国的住民写下的日记。JOI 教授为了通过这份日记来研究古代 IOI 国的生活,开始着手调查日记中记载的事件。
日记中记录了连续 N 天发生的事件,大约每天发生一件。
事件有种类之分。第 i 天发生的事件的种类用一个整数 Xi 表示,Xi 越大,事件的规模就越大。
JOI 教授决定用如下的方法分析这些日记:
-
选择日记中连续的几天 [L,R] 作为分析的时间段;
-
定义事件 A 的重要度 WA 为 A×TA,其中 TA 为该事件在区间 [L,R] 中出现的次数。
现在,您需要帮助教授求出所有事件中重要度最大的事件是哪个,并输出其重要度。
注意:教授有多组询问。
输入格式
第一行两个空格分隔的整数 N 和 Q,表示日记一共记录了 N 天,询问有 Q 次。
接下来一行 N 个空格分隔的整数表示每天的事件种类。
接下来 Q 行,每行给出 L,R 表示一组询问。
输出格式
输出共有 Q 行,每行一个整数,表示对应的询问的答案。
输入输出样例
输入 #1
5 5 9 8 7 8 9 1 2 3 4 4 4 1 4 2 4
输出 #1
9 8 8 16 16
光从题目上看,我们只需要暴力跑一遍莫队。然后依次求贡献就可以了,但是我们并不好维护res,因为我们对找到最大值是很方便的,但是一但区间长度减小,我们不知道次大值是多少,所以,就我看来,回滚莫队是比带修莫队复杂很多的。
所以,分块的重要性就更加明显,为了节省不必要的时间浪费,我们尽可能地先将left在同一区块的进行操作,同时我们会发现两种情况:
情况一:right也在left所在区块内,此时我们就不打算对其进行位移操作,直接对其进行暴力获取。
LL calc(int l, int r)
{
LL mx = 0;
for(int i = l; i <= r; i ++)
c[a[i]] = 0;
for(int i = l; i <= r; i ++)
{
++ c[a[i]];
mx = max(mx, c[a[i]] * b[a[i]]);
}
return mx;
}
if(pos[q[x].l] == pos[q[x].r])
{
ans[q[x].id] = calc(q[x].l, q[x].r);
continue;
}
情况二:right不在left所在区块,我们发现对于这种排序后的查询,我们查询区间的右端点是不断递增的,而左端点则是无序的,需要我们每次查询完成后将左端点移回原位置,所以我们舍弃Sub操作,只需用Add向右进行寻找,当我们的right脚离开区间时,我们先将右脚进行拓展,得到一个res值,记录为last,然后我们将左脚向左拓展得到res,因为我们的res是全局变量,此时我们的res就是我们要询问区间的答案,然后我们进行回滚,将左端点移回原位置,同时将刚刚向左扩展时统计的cnt值复原,代码如下:
while(q[x].r > r)
Add(a[++ r]);
last = res;
while(q[x].l < l)
Add(a[-- l]);
ans[q[x].id] = res;
while(l <= div)
--cnt[a[l ++]];
res = last;
同一区块的查询结束后,我们将查询区域移至下一区块,重复上述步骤。
针对例题,我们可以很容易地写出Add函数:
void Add(int temp)
{
++ cnt[temp];
res = max(res, cnt[temp] * b[temp]);
}
因为本题的数据量比较大,我们使用lower_bound函数进行离散化处理:
for(int i = 1; i <= n; i ++)
a[i] = lower_bound(b + 1, b + n + 1, a[i]) - b;
最后附上完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
struct node{
int l;
int r;
int id;
}q[N];
int n, m, siz, pos[N];
LL a[N], b[N], ans[N], cnt[N], c[N];
LL res, last;
bool cmp(const node x, const node y)
{
if(pos[x.l] != pos[y.l])
return x.l < y.l;
return x.r < y.r;
}
void Add(int temp)
{
++ cnt[temp];
res = max(res, cnt[temp] * b[temp]);
}
LL calc(int l, int r)
{
LL mx = 0;
for(int i = l; i <= r; i ++)
c[a[i]] = 0;
for(int i = l; i <= r; i ++)
{
++ c[a[i]];
mx = max(mx, c[a[i]] * b[a[i]]);
}
return mx;
}
int main()
{
cin >> n >> m;
siz = sqrt(n);
for(int i = 1; i <= n; i ++)
{
cin >> a[i];
b[i] = a[i];
pos[i] = (i - 1) / siz + 1;
}
int num = pos[n];
sort(b + 1, b + n + 1);
for(int i = 1; i <= n; i ++)
a[i] = lower_bound(b + 1, b + n + 1, a[i]) - b;
for(int i = 1, l, r; i <= m; i ++)
{
cin >> l >> r;
q[i] = {l, r, i};
}
sort(q + 1, q + m + 1, cmp);
for(int i = 1, x = 1; i <= num; i ++)
{
res = 0;
last = 0;
for(int j = 1; j <= n; j ++)
cnt[j] = 0;
int div = min(siz * i, n), l = div + 1, r = div;
for(;pos[q[x].l] == i; x ++)
{
if(pos[q[x].l] == pos[q[x].r])
{
ans[q[x].id] = calc(q[x].l, q[x].r);
continue;
}
while(q[x].r > r)
Add(a[++ r]);
last = res;
while(q[x].l < l)
Add(a[-- l]);
ans[q[x].id] = res;
while(l <= div)
--cnt[a[l ++]];
res = last;
}
}
for(int i = 1; i <= m; i ++)
cout << ans[i] << endl;
}
总结:
相比于带修莫队的简单修改,回滚莫队的思维深度更深,更加抽象,更不容易理解,我也是想了整整一天才想明白,最主要的部分是分块对每一个区块的查询进行独立处理,还有为什么可以只扩展区间而不用缩减区间,因为回滚的过程其实就是缩减的过程,只不过每次缩减后的位置都一样罢了,最重要的是回滚的步骤需要理解,什么时候要存last也是很重要的。