P2263 命运的彼方 题解

题目传送门

双倍经验

写在前面:

  • 本文使用了 FHQ_Treap 求解,若读者还不会 FHQ_Treap 可自行前往 OI Wiki 无旋 treap 学习或者跳过代码部分。
  • 下文记 \(n,n-k+1\) 同阶为 \(n\)

题目大意:

给定一个长度为 \(n\) 的序列,每次可以将其中一个数 \(+1\) 或者 \(-1\),求最小的次数使得该序列上最少有连续 \(k\) 个相同的数。

数据范围:\(1\leq n \leq 5 \times 10^5,2 \leq k \leq n, 0 \leq H_i \leq 10^{12}\)

题目分析:

先考虑一个长度为 \(k\) 的序列将所有数变成相同的最小次数。

不难发现,变成该序列的中位数的次数最小。

证明如下:

假设序列长为 \(2n+1\),将序列从小到大排好,那么中位数为 \(mid=H_{n+1}\),比中位数小的数有 \(n\) 个,比中位数大的数也有 \(n\) 个,不妨设:

\[sumL=\sum_{i=1}^{n} (mid-H_i),\\sumR=\sum_{n+2}^{2n+1} (H_i-mid) \]

那么如果将整个序列变成 \(mid\),其代价为 \(ans_0=sumL+sumR\)

如果将整个序列变成不是 \(mid\) 的数,例如 \(mid-1\),其代价为

\[\begin{aligned} ans_x&=(sumL-n)+(sumR+n)+1\\ &=sumL+sumR+1\\ &>sumL+sumR=ans_0\\ \end{aligned}\]

,其余同理,都能证明 \(ans_x>ans_0\)

所以,当变成整个序列的中位数的时候,次数最小。

同理可得,当序列长为 \(2n\) 时,将序列从小到大排好,那么中位数为 \(mid=(H_n+H_{n+1})/2\),证明与序列长为 \(2n+1\) 的证明相似,可以得出相同的结论。

再回到题目,显然是要在长度为 \(n\) 的序列中枚举每一个长度为 \(k\) 的区间求出对应的最小次数 \(res_i=\sum_{j=i}^{i+k-1} |H_j-mid_i|\),那么最终的答案为 \(ans=\min_{i=1}^{n-k+1} res_i\),瓶颈在于如何快速求动态区间的中位数(动态 kth)

想到这里,可以感觉到有点与平衡树沾上边了,我们可以先把区间 \([L_1,R_1]\) 预处理放在平衡树上,每次求区间 \([L_i,R_i]\) 的答案时,只需要删除 \(H_{L_{i-1}}\) 以及增加 \(H_{R_{i}}\) 即可,这里选用了 FHQ_Treap 按权值分裂求解。

每一次将平衡树分裂成三部分:\([L,mid-1],(mid-1,mid],(mid,R]\),此时 \(siz_L\) 就是小于中位数的个数,\(siz_R\) 就是大于中位数的个数,那么

\[res=siz_L \times mid - sum_L + sum_R - siz_R \times mid \]

其余与 FHQ_Treap 模板一致,注意本题数据会超 int,记得开 long long

(不过真的不建议跟我一样懒打了 #define int long long

时间复杂度:\(O(n \log n)\),由于 FHQ_Treap 自带较大常数所以跑得有点慢了。

Tip:本代码不开 O2 似乎是跑不过去的(TLE 90pts),建议优化常数或者开 O2。

Code:

#include<bits/stdc++.h>
#define int long long//记得开 long long !!!
#define ls(u) t[u].ls
#define rs(u) t[u].rs
using namespace std;
const int N=5e5+5;
const int INF=1e18;
int n,k,root,cnt;
int a[N],ans=INF;//初始值赋一个较大值
struct FHQ_Treap
{
	int ls,rs;
	int val,sum;
	int pri,siz;
}t[N];
void update(int u)
{
	t[u].siz=t[ls(u)].siz+t[rs(u)].siz+1;
	t[u].sum=t[ls(u)].sum+t[rs(u)].sum+t[u].val;
}
int newNode(int x)
{
	int u=++cnt;
	t[u].ls=t[u].rs=0;
	t[u].val=t[u].sum=x;
	t[u].pri=rand();
	t[u].siz=1;
	return u;
}
void split(int u,int x,int &L,int &R)
{
	if(!u)
	{
		L=R=0;
		return;
	}
	if(t[u].val<=x)//按权值分裂
	{
		L=u;
		split(rs(u),x,rs(u),R);
	}
	else
	{
		R=u;
		split(ls(u),x,L,ls(u));
	}
	update(u);
}
int merge(int L,int R)
{
	if(!L||!R) return L|R;
	if(t[L].pri>t[R].pri)
	{
		t[L].rs=merge(t[L].rs,R);
		update(L);
		return L;
	}
	else
	{
		t[R].ls=merge(L,t[R].ls);
		update(R);
		return R;
	}
}
void insert(int x)//插入
{
	int L,R;
	split(root,x,L,R);
	root=merge(merge(L,newNode(x)),R);
}
void del(int x)//删除
{
	int L,R,p;
	split(root,x,L,R);
	split(L,x-1,L,p);//分成 [L,mid-1],(mid-1,mid],(mid,R]三部分
	p=merge(t[p].ls,t[p].rs);//只删除等于 x 的一个数
	root=merge(merge(L,p),R);
}
int kth(int u,int k)//求排名为 k 的值在平衡树上的位置
{
	if(k==t[ls(u)].siz+1) return u;
	if(k<=t[ls(u)].siz) return kth(ls(u),k);
	if(k>t[ls(u)].siz) return kth(rs(u),k-t[ls(u)].siz-1);
}
int query(int x)
{
	int L,R,p;
	split(root,x,L,R);
	split(L,x-1,L,p);
	int ans=t[L].siz*x-t[L].sum+t[R].sum-t[R].siz*x;//详细证明在上面已提到
	root=merge(merge(L,p),R);
	return ans;
}
signed main()
{
	srand(time(0));
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=n;i++)	
		scanf("%lld",&a[i]);
	for(int i=0;i<k;i++)//预处理将 [0,k) 插入平衡树
		insert(a[i]);
	for(int i=k;i<=n;i++)
	{
		int mid;
		del(a[i-k]),insert(a[i]);//区间 [L,R]
       //分类讨论
		if(k&1) mid=t[kth(root,k/2+1)].val;//若 k 为奇数,mid=a[k/2+1]
			else mid=(t[kth(root,k/2)].val+t[kth(root,k/2+1)].val)/2;//若 k 为偶数,mid=(a[k/2]+a[k/2+1])/2
		int res=query(mid);
		if(res<ans)
			ans=res;
	}
	printf("%lld\n",ans);
	return 0;
}

写在后面:

update 24/8/13:修改了部分 \(\LaTeX\) 公式,让其排版更加舒服(bushi)

posted @ 2024-10-21 11:26  lunjiahao  阅读(1)  评论(0编辑  收藏  举报