Luogu P1627 [CQOI2009] 中位数

本文讨论了如何将一个关于计算排列中中位数子序列的问题,从暴力的O(n^3)复杂度优化到高效的O(n)复杂度。首先介绍了暴力枚举和前缀和优化的方法,然后揭示了一个巧妙的算法,通过修改数组并利用前缀和统计满足条件的子序列。

[CQOI2009] 中位数

题目描述

给出 1,2,...,n1,2,...,n1,2,...,n 的一个排列,统计该排列有多少个长度为奇数的连续子序列的中位数是 bbb。中位数是指把所有元素从小到大排列后,位于中间的数。

输入格式

第一行为两个正整数 nnnbbb,第二行为 1,2,...,n1,2,...,n1,2,...,n 的排列。

输出格式

输出一个整数,即中位数为 bbb 的连续子序列个数。

样例 #1

样例输入 #1

7 4
5 7 2 4 3 1 6

样例输出 #1

4

提示

数据规模与约定

对于 30%30\%30% 的数据中,满足 n≤100n \le 100n100

对于 60%60\%60% 的数据中,满足 n≤1000n \le 1000n1000

对于 100%100\%100% 的数据中,满足 n≤100000,1≤b≤nn \le 100000,1 \le b \le nn100000,1bn

分析

首先,第一个想法,暴力枚举。第一层枚举长度,第二层枚举第一个数的下标,第三层遍历,算法复杂度 O(n3)O(n^3)O(n3) ,实现较为简单,这里就不打了。

然后,发现遍历其实只是查找区间内大于 bbb 和小于 bbb 的数是否一样多,因此可以前缀和预处理,算法复杂度降至 O(n2)O(n^2)O(n2)80pts80pts80pts,代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,b,a[100010],p,lr,hr,st,ed,ans=0;//lr:lower,hr:higher,意为大于/小于b的数个数
int main(){
	cin>>n>>b;
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i]);
		if(a[i]==b) p=i;
	}
	for(int i=1;i<=n;i+=2){
		st=max(1,p-i+1),ed=min(n-i+1,p),lr=0,hr=0;;
		for(int j=0;j<i;++j) lr+=(a[j+st]<b),hr+=(a[j+st]>b);
		for(int j=st;j<=ed;++j){
			ans+=(lr==hr);
			lr-=(a[j]<b),lr+=(a[j+i]<b),hr-=(a[j]>b),hr+=(a[j+i]>b);
		}
	}
	cout<<ans<<endl;
	return 0;
}

注:这里,类似 (a[j+st]<b)(a[j+st]<b)(a[j+st]<b) 的打法,其实就是:若括号内式子成立,则值为 111 ;否则值为 000

枚举能给的分只有这么多,接下来的算法非常的巧妙(题出的好),可以 O(n)O(n)O(n) 解决这个问题:

首先,发现每个数对解决问题的贡献只有其大于或小于 bbb ,于是将原数组改成:

for(int i=1;i<=n;++i){
	if(a[i]>b) a[i]=1;
	else if(a[i]<b) a[i]=-1;
	else a[i]=0;
}

然后,从要求的东西出发:对于一个符合条件的子序列,其大于 bbb 的数的个数和小于 bbb 的数的个数要求相等。想一想,是否意味着:对于我们刚刚改好的 aaa 数组,一段包括 a[i]=0a[i]=0a[i]=0 在内,且总和为 000 的子序列,它是不是就是一个满足题意的子序列?是。为什么?因为它大于 bbb 的数的个数和小于 bbb 的数的个数相等(即改好的aaa数组中 111−1-11 的个数相同)。

进一步,总和为 000 意味着我们可以用前缀和来维护一段区间的和。设前缀和数组为 sssbbb (亦即 a[i]=0a[i] =0a[i]=0)所在位置为 ppp ,那么对于任意一段区间 [[[ lll ,,, rrr ]]] ,只要满足 s[l−1]==s[r]s[l-1]==s[r]s[l1]==s[r] ,且 l−1≤pl-1\le pl1p && p≤rp\le rpr,那么 [[[ lll ,,, rrr ]]] 就是一段合法子序列。

于是,问题转化成:s0s_0s0 ~ sp−1s_{p-1}sp1sps_psp ~ sns_nsn 两组数中相同的数字对数

那么这个问题如何解决呢?注意到 sss 数组中的数都属于 [−n,n][-n,n][n,n] ,因此不妨打个哈希表存储每个数字在 s0s_0s0 ~ sp−1s_{p-1}sp1sps_psp ~ sns_nsn 两组数中各出现了多少次,最后结果就是:
∑i=−nnvh[i][0]∗vh[i][1]\sum_{i=-n}^n vh[i][0]*vh[i][1]i=nnvh[i][0]vh[i][1]
注:由于数组没有负下标一说,所以可将 vhvhvh 的第一维的下标加上 100000100000100000

Code

#include<bits/stdc++.h>
using namespace std;
bool f=1;
int n,b,a[100010],s[100010];
long long ans=0;
int vh[200010][2];
int main(){
	cin>>n>>b;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		if(a[i]<b) a[i]=-1;
		else if(a[i]>b) a[i]=1;
		else a[i]=0,f=0;
		s[i]=s[i-1]+a[i];
		if(!f) vh[s[i]+100000][1]++;
		else vh[s[i]+100000][0]++;
	}
	vh[100000][0]++;//s[0]处的一个0别忘了加上!
	for(int i=0;i<=200000;++i) ans+=vh[i][0]*vh[i][1];
	cout<<ans<<endl;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值