莫队算法拓展(回滚&不删除莫队)

        上一篇讲了带修莫队的具体内容,在我看来带修莫队的思路比较清晰,无非是在跑区间的过程中加入元素的修改,只需要考虑查询与修改的时间相对性就可以了。这次要分享的是另一种莫队算法的拓展——回滚&不删除莫队。

        还是先贴一道经典例题

https://www.luogu.com.cn/problem/AT_joisc2014_cicon-default.png?t=N7T8https://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也是很重要的。

  • 36
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值