数据结构专题-学习笔记:莫队#3(回滚莫队,莫队二次离线)

回顾:

一个莫队,六种方法(普通莫队、带修莫队、树上莫队、树上带修莫队、回滚莫队/不删除莫队、莫队二次离线/第十四分块(前体)),连续写了三篇博文来讲述。本篇博文是最后一篇,将会讲述最后两种莫队:[回滚莫队/不删除莫队] [莫队二次离线/第十四分快(前体)],同时将会总结六种莫队算法。

3.练习题

题单:(前面的就略去了)

回滚莫队/不删除莫队

学到了这里,各位会发现:莫队支持查询,修改,也支持删除(当然不是直接删除一个数)。但是,对于一些题目,普通莫队就过不去了,因为你会发现:del 函数非常难写,可能写着写着就变成了 \(O(n)\) 暴力删除,这是莫队所不能容忍的。还不如直接 \(O(n^2)\) 暴力呢?于是,针对这种问题,回滚莫队/不删除莫队就出现了。

AT1219 歴史の研究

事实上,我个人认为这道题更像回滚莫队/不删除莫队的模板题,因此我拿它来做讲解。

这题的 add 函数很好打,但是 del 函数很不好打,因此我们要另寻他法。

我们回顾一下莫队的三个优化:

优化一:使用 \(cnt\) 数组省略值域。貌似没有什么用处qwq。

优化二:使用 \(l,r\) 指针移来移去。带修莫队已经用过了。

优化三:应用分块思想进行排序,不使用奇偶性排序的情况下左端点在同一块的询问右端点按照从小到大排序。唉等等,这个优化好像还没有用过,我们是不是可以使用这个优化写出对回滚莫队/不删除莫队呢?当然可以!

考虑分块思想。设 \(block\) 表示块长,\(bnum\) 表示块的数量,那么对于每一块块内的询问,直接暴力出奇迹即可。

那么针对于那些跨越块数的呢?

想想当初分块的时候,我们顺序处理整块,那么这里我们也顺序处理一下:

对于第 \(i\) 快,我们指定 \(l=i*block+1,r=l-1\) 。根据上面所述, \(r\) 只需要往右边移动即可,但是不能保证 \(l\) 只往左移动,因此我们还得想办法解决 \(l\) 的问题。

这里就是一个指针移动顺序的问题了。

我们有四个顺序:\(l++,l--,r--,r++\) ,前面四种莫队随便乱搞都能过,但是回滚莫队/不删除莫队不能随便乱搞。首先,\(r--\) 不可能。其次,为了保证 “不删除” ,我们需要规定 \(r++\) 第一个操作(因为 \(r\) 不可能回去)。将当前操作完的答案临时存在 \(tmp\) 里面。然后,\(l--\) ,算出答案 \(total\)在算完答案之后,我们需要 \(l++\) ,并且让 \(total=tmp\) 啥意思?

因为这里我们要保证 “不删除”,所以我们不能在 \(l++\) 的时候维护答案。最好的办法就是将 \(l\) 移回到 \(i*block+1\) ,同时由于我们先处理了 \(r++\) ,因此我们可以保证 \(tmp\)\([i*block+1,r]\) 之间的答案。因此,将 \(l\) 移回去,同时路上消除 \(cnt\) 的影响(要统计个数),而 \(total=tmp\) 能够保证在 “不删除” 的情况下存储答案。

以上就是回滚莫队/不删除莫队的主要思路。

代码里面需要注意:暴力小块时用的 \(cnt\) 不能与莫队大块时用的 \(cnt\) 一起,否则很容易导致 WA。同时绝对不能使用奇偶性排序!

说每暴力一次都 memset 的人可以看一看这组数据:

n=10000,block=sqrt(n)=100;
询问:[1,2][2,3]······
询问 9999 个,每一次都memset一遍,时间复杂度直接飙升到 O(n^2)
TLE 在向你招手!

不要忘记 long long ,离散化。

代码:

#include<bits/stdc++.h>
using namespace std;

const int MAXN=2e5+10;
typedef long long LL;
int n,m,a[MAXN],b[MAXN],block,ys[MAXN],lastn,bnum;
LL total,ans[MAXN];
map<int,int>cnt,cnt2;//懒得写离散化,用 map 代替
struct node
{
	int l,r,id;
}q[MAXN];

int read()
{
	int sum=0;char ch=getchar();
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') {sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
	return sum;
}

bool cmp(const node &fir,const node &sec)
{
	if(ys[fir.l]^ys[sec.l]) return ys[fir.l]<ys[sec.l];
	return fir.r<sec.r;
}

int main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++) cnt[a[i]=read()]=0;
	block=sqrt(n);bnum=ceil((double)n/block);
	for(int i=1;i<=n;i++) ys[i]=(i-1)/block+1;
	for(int i=1;i<=m;i++) {q[i].l=read();q[i].r=read();q[i].id=i;}
	sort(q+1,q+m+1,cmp);
	int j=1;
	for(int i=1;i<=bnum;i++)
	{
		cnt.clear();
		int l=i*block+1,r=l-1;
		LL tmp=0;total=0;
		for(;ys[q[j].l]==i;j++)
		{
			if(ys[q[j].l]==ys[q[j].r])
			{
				cnt2.clear();tmp=0;
				for(int k=q[j].l;k<=q[j].r;k++) {cnt2[a[k]]++;tmp=max(tmp,1ll*a[k]*cnt2[a[k]]);}
				ans[q[j].id]=tmp;
				continue;
			}
			while(r<q[j].r) {++r;cnt[a[r]]++;total=max(total,1ll*a[r]*cnt[a[r]]);}
			tmp=total;
			while(l>q[j].l) {--l;cnt[a[l]]++;total=max(total,1ll*a[l]*cnt[a[l]]);}
			ans[q[j].id]=total;
			while(l<i*block+1) {cnt[a[l]]--;l++;}
			total=tmp;
		}
	}
	for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
	return 0;
}

莫队二次离线/第十四分块(前体)

参照这篇博文:数据结构专题-学习笔记:莫队二次离线

4.总结

终于到总结了qwq。

如果真的掌握了莫队,莫队其实还是很简单的。

这里再放一放 6 种莫队的主要思路吧!

  1. 普通莫队:两个指针 \(l,r\) 在序列上动,排序以左端点所在块为第一关键字,右端点为第二关键字排序,可以使用奇偶性优化加快排序。
  2. 带修莫队:加一维指针 \(t\) ,让第三个指针在时间轴上动。
  3. 树上莫队:用欧拉序将树上问题转变成区间问题
  4. 树上带修莫队:前两者的结合,用欧拉序转变成区间问题后使用带修莫队的套路。
  5. 回滚莫队/不删除莫队:借助分块思路,块内暴力,块外指针移动,同时记录 \([i*block+1,r]\) 的答案,但是绝对不能使用奇偶性排序。
  6. 莫队二次离线/第十四分块(前体):答案可以转换为 \(h([l,r])=h([1,r])-h([1,l-1])-g([1,l-1],[l,r])\)

最后再放一下三篇博文的链接:

数据结构专题-学习笔记:莫队#1(普通莫队):普通莫队。

数据结构专题-学习笔记:莫队#2(带修莫队,树上莫队):带修莫队,树上莫队,树上带修莫队。

数据结构专题-学习笔记:莫队#3(回滚莫队,莫队二次离线):回滚莫队/不删除莫队,莫队二次离线/第十四分块(前体)。

有兴趣的读者也可以看一下 洛谷日报 #183 期:你以为莫队只能离线?莫队的在线化改造,增强对莫队的理解与自己的水平。(所以以后很多树套树的题目都能用在线莫队切掉了?)

posted @ 2022-04-13 21:34  Plozia  阅读(141)  评论(0编辑  收藏  举报