洛谷 P4062 - [Code+#1]Yazid 的新生舞会 的线性做法

洛谷题面传送门

一个线性做法。

\(n\log n\) 解法可以戳这里查看

首先回顾一下 \(n\log n\) 解法的过程:我们对于每一个数 \(x\),考察其出现位置,设为 \(t_1,t_2,t_3,\cdots,t_c\),然后在这些位置上填上 \(1\),其余位置上填上 \(-1\),然后对序列做一遍前缀和,那么该数对答案的贡献就是前缀和数组中顺序对个数。

直接 \(n\log n\) 求复杂度好像有一点高,怎样优化复杂度呢?首先注意到每个可能成为区间右端点的位置并不多,具体来说,我们猜测它是 \(\mathcal O(c)\) 级别的,其中 \(c\)\(x\) 在原序列中的出现次数,也就是说,我们称前缀和数组为 \(s_k\),那么满足 \(s_k>\min\limits_{j=1}^{k-1}s_j\)\(k\) 的总个数是线性的,因此我们考虑每次计算一个连续递减段的贡献时,暴力向后枚举,直到 \(s_k\le\min\limits_{j=1}^{k-1}s_j\) 即可,这样我们只用实现区间加区间求和(求前缀和),即二阶差分数组上单点加,求二阶差分数组的二阶前缀和即可。

到这里,直接做的复杂度还是带 log 的。我们还能注意到这样一个事实:\(s\) 数组中相邻两个元素之差为 \(\pm 1\)。因此我们考虑动态维护一个指针 \(p\),并动态维护二阶差分数组在 \((-\infty...p-1]\) 处的一阶、二阶前缀和,这样向上移动指针时就加入当前位置的贡献,否则扣除掉当前位置的贡献,这样就不用 BIT 之类的东西维护前缀和。直接移动指针显然会被卡到平方,不过发现对于一段连续递减段,我们假设暴力向后枚举到的位置为 \(q\),那么由于对于 \(q+1\) 到该连续段右端点 \(r\) 这段区间内任意一个 \(k\),都有 \(s_k>\min\limits_{j=1}^{k-1}s_j\),因此这部分的位置在移动指针的过程中肯定是空的,直接一路移下去不加任何贡献即可。这样需要加贡献的部分就是待查询的部分,外加上 \(x\)\(c\) 个出现位置 \(t_1,t_2,t_3,\cdots,t_c\),总个数是 \(\Theta(c)\) 级别的,因此总复杂度就是 \(\Theta(\sum c)=\Theta(n)\)

话说这题我好像已经写了四种不同复杂度/不同常数的做法了?\(n\sqrt{n\log n}\) 分块,\(n\log n\) 线段树,\(n\log n\) BIT,\(\mathcal O(n)\) 指针维护。看来此题确实是一道很值得研究的题目。

代码(目前洛谷最优解,335ms):

using namespace fastio;
const int MAXN=5e5;
const int DLT=MAXN+3;
int n,fuck,a[MAXN+5],hd[MAXN+5],val[MAXN+5],nxt[MAXN+5],item_n=0;
void ins(int x,int y){val[++item_n]=y;nxt[item_n]=hd[x];hd[x]=item_n;}
int d[MAXN*2+10];
inline void add(int l,int r,int v){d[l+DLT]+=v;d[r+DLT+1]-=v;}
int main(){
	read(n);read(fuck);add(0,0,1);ll res=0;
	for(int i=1;i<=n;i++) read(a[i]),ins(a[i],i);
	for(int v=0;v<n;v++) if(hd[v]){
		static int pos[MAXN+5],sum[MAXN+5];int cnt=-1;
		pos[++cnt]=n+1;
		for(int e=hd[v];e;e=nxt[e]) pos[++cnt]=val[e];
		pos[++cnt]=0;reverse(pos,pos+cnt+1);sum[0]=0;
		if(cnt==2){res++;continue;}
		int mnp=0;ll sum1=0,sum2=0;
		for(int i=1;i<=cnt;i++){
			int l=sum[i-1]-(pos[i]-pos[i-1]-1),r=sum[i-1]-1;
			if(l<=r){
				int lim=max(l,mnp);
				for(int j=r;j>=lim;j--){
					sum2-=sum1;sum1-=d[j+DLT];
					res+=sum2;
				}
			} if(i==cnt) break;
			add(l,r,1);sum1+=d[l+DLT];sum2+=sum1;
			sum[i]=l+1;add(sum[i],sum[i],1);res+=sum2;
			chkmin(mnp,l);
		}
		for(int i=1;i<cnt;i++) add(sum[i]-1,sum[i-1]-1,-1),add(sum[i],sum[i],-1);
	} printf("%lld\n",res);
	return 0;
}
posted @ 2021-10-20 09:44  tzc_wk  阅读(72)  评论(0)    收藏  举报