「JOISC 2014 Day1」历史研究 --- 回滚莫队

题目又臭又长,但其实题意很简单。

给出一个长度为\(N\)的序列与\(Q\)个询问,每个询问都对应原序列中的一个区间。对于每个查询的区间,设数\(X_{i}\)在此区间出现的次数为\(Sum_{X_{i}}\),我们需要求出对于当前区间\(X_{i}*Sum_{X_{i}}\)的最大值。

数据范围:\(1\leq N,Q\leq10^{5},1\leq X_{I}\leq1 0^{9}\)


众所周知,对于没有修改的区间查询问题且数据范围在\(1e5\)的题目,我们首先就可以考虑使用莫队来解决,事实上这道题也是可以用莫队来解决的,不过需要一点变形。

对每个询问进行分块排序后,使用莫队算法。将原序列的数进行离散化,记每个数出现的次数为\(Cnt_{x}\),当前莫队的左右下标为\(l\)\(r\),当前最优答案为\(sum\)

我们发现当\(l\)减少和\(r\)增加时的情况很好更新答案(也就是原莫队的\(add\)操作,与之对应的删除操作也就是\(del\)),只需要\(Cnt_{r}++\),然后求一个最大值\(max(sum,Cnt_{r})\)

但是\(del\)操作就很难实现。如果删除的数对应的是最大值,也就是在\(sum=Cnt_{r}*X_{r}\)中,\(X_{r}--\)了,那么我们就不能保证当前的\(sum\)是最大的。考场上笔者想到的解决方法是维护一个次大值,但可以发现如果要维护次大值那还要维护一个第3大值。。。

考场上笔者是使用的线段树来解决这个问题,维护每个\(Cnt_{i}*X_{i}\)的最大值,时间复杂度对应的是\(O(n \sqrt{n}log_{n})\)。尽管理论上可以过,卡常之后也确实可以过,但并不完美因为我不会卡常。这里是常数比较大的笔者的40分套线段树代码。

#include <iostream>
#include <cstdio>
#include <map>
#include <algorithm>
#include <cmath>
using namespace std;

#define LL long long
const int N=100020;

struct node {
	LL p,sum;
	node() {};
	node(LL S,LL P) { p=P; sum=S; }
}; 

LL ans[N],maxx[N<<4],M2[N],n,m,A[N],T,cnt;
map<LL,LL> M1;

struct Qu {
	int l,r,p;
	bool operator < (const Qu &nxt) {//分块排序
		if(l/T+1 != nxt.l/T+1) return l/T+1<nxt.l/T+1;
		else return r<nxt.r;
	}
}tzy[N];

void insert(int i,int l,int r,int x,int t) {//线段树维护
	if(l==r) {
		maxx[i]+=t;
		return ;
	}
	int mid=(l+r)>>1;
	if(x<=mid) insert(i<<1,l,mid,x,t);
	else insert(i<<1|1,mid+1,r,x,t);
	maxx[i]=max(maxx[i<<1],maxx[i<<1|1]);
}

void add(int now) {
	insert(1,1,cnt,A[now],M2[A[now]]);
}

void del(int now) {
	insert(1,1,cnt,A[now],-M2[A[now]]);
}

int main() {
	cin>>n>>m; T=sqrt(n);
	for(int i=1;i<=n;i++) {
		scanf("%lld",&A[i]);	
		if(!M1[A[i]]) {
			M1[A[i]]=++cnt;	
			M2[cnt]=A[i];
		}
		A[i]=M1[A[i]];
	}
//	cout<<endl<<cnt<<endl;
	for(int i=1;i<=m;i++) {
		scanf("%d%d",&tzy[i].l,&tzy[i].r);
		tzy[i].p=i;
	}
	sort(tzy+1,tzy+1+m);
	int l=1,r=1; insert(1,1,cnt,A[1],M2[A[1]]);
//	for(int i=1;i<=m;i++) { cout<<tzy[i].l<<' '<<tzy[i].r<<endl;}
	for(int i=1;i<=m;i++) {
		while(r<tzy[i].r) add(++r);
		while(l>tzy[i].l) add(--l);
		while(r>tzy[i].r) del(r--);
		while(l<tzy[i].l) del(l++);
		ans[tzy[i].p]=maxx[1];
	}
	for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
}

另一种思路其实是值得我们学习的。如果\(del\)操作不好实现,那为什么我们不能跳出优化\(del\)操作的框框,而是想办法将问题转化为只使用\(add\)操作呢?

于是便有了莫队算法的变形:回滚莫队,适用于不好维护\(del\)操作的情况,且它的时间复杂度也十分优美\(O(n \sqrt {n})\)(不像上一个又臭又长)。

回滚莫队可以说是将分块的思想用到了极致。我们将问题一个块一个块的处理。因为我们是将询问的\(r\)从小到大排序,所以对于同一块的处理,可以保证\(r\)的转移是\(O(n)\)的,且只涉及到\(add\)操作。

但是\(l\)就不好处理了,因为它的排列相较于\(r\)的排列是无序的。但因为是分块排列,我们可以保证从当前块的右端点\(R_{i}\),到\(l\),最多只有\(\sqrt {n}\)步。

于是我们便可以考虑这样一种策略。每次处理都将\(l\)移到\(R_{i}\),这样可以保证\(l\)\(r\)都只有\(add\)操作。当前询问处理完后,只需要将\(l\)“滚”回来。因为\(l\)的转移最多为\(\sqrt {n}\)次,\(r\)转移最多为\(n\)次,所以复杂度为\(O(n \sqrt{n})\)

思路讲完了,但还有种特殊情况需要处理,当\(L_{i}\leq l,r\leq R_{i}\)时,也就是当前询问左右端点都处于同一个块时运用上述方法并不好处理。因为可以保证\(r-l \leq \sqrt{n}\),所以只需要暴力跑一边即可。

细节比较多,详情见代码。

#include <iostream>
#include <cstdio>
#include <map>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;

#define LL long long
const int N=100020;

LL ans[N],B[N],q,cnt[N],block[N],L[N],R[N],Cntl,n,sum,m,p,A[N],T;

struct Qu {
	int l,r,p;
	bool operator < (const Qu &nxt) {
		return block[l]==block[nxt.l] ? r<nxt.r:l<nxt.l;
	}
}tzy[N];

void divide() {
	for(int i=1;i<=q;i++) L[i]=R[i-1]+1,R[i]=L[i]+T-1;
	if(R[q]<n) L[q+1]=R[q]+1,R[q+1]=n,q++;
	for(int i=1;i<=n;i++) block[i]=(i-1)/T+1;
}

void add(int x) {
	cnt[A[x]]++;
	if(cnt[A[x]]*B[A[x]]>sum) sum=cnt[A[x]]*B[A[x]];
} 

LL solve(int l,int r) {
	static int c[N]; 
	LL solu=0;
	for(int i=l;i<=r;i++) ++c[A[i]];
	for(int i=l;i<=r;i++) solu=max(solu,c[A[i]]*B[A[i]]);
	for(int i=l;i<=r;i++) --c[A[i]];
	return solu;
}

int main() {
	cin>>n>>m; T=sqrt(n); q=n/T;
	for(int i=1;i<=n;i++) scanf("%lld",&A[i]),B[i]=A[i];
	for(int i=1;i<=m;i++) scanf("%d%d",&tzy[i].l,&tzy[i].r),tzy[i].p=i;
	sort(B+1,B+1+n);
	p=unique(B+1,B+1+n)-B-1;
//	cout<<endl<<cnt<<endl;
	for(int i=1;i<=n;i++) A[i]=lower_bound(B+1,B+1+p,A[i])-B;
	divide();
	sort(tzy+1,tzy+1+m);
	
	for(int i=1,l,r,j=1;i<=q;i++) {
		r=R[i]; sum=0;
		memset(cnt,0,sizeof(cnt));
		while(block[tzy[j].l]==i) {
			l=R[i]+1;
			if(tzy[j].r-tzy[j].l<=T)
				ans[tzy[j].p]=solve(tzy[j].l,tzy[j].r),++j;
			else {
				while(r<tzy[j].r) add(++r);
				LL tmp=sum;
				while(l>tzy[j].l) add(--l);
				ans[tzy[j].p]=sum;
				sum=tmp;
				while(l<=R[i]) --cnt[A[l++]];
				++j;
			}
		}
	}
	for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
}

考试还有一个每个节点为\(4*4\)的矩阵的题没有写。对于我这种一个月一更的作者,写个博客要一上午的人。。。还早呢

posted @ 2020-01-05 11:37  MisakaMKT  阅读(207)  评论(0编辑  收藏  举报