[CQOI2009] 中位数
题目描述
给出 1,2,...,n1,2,...,n1,2,...,n 的一个排列,统计该排列有多少个长度为奇数的连续子序列的中位数是 bbb。中位数是指把所有元素从小到大排列后,位于中间的数。
输入格式
第一行为两个正整数 nnn 和 bbb,第二行为 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 100n≤100;
对于 60%60\%60% 的数据中,满足 n≤1000n \le 1000n≤1000;
对于 100%100\%100% 的数据中,满足 n≤100000,1≤b≤nn \le 100000,1 \le b \le nn≤100000,1≤b≤n。
分析
首先,第一个想法,暴力枚举。第一层枚举长度,第二层枚举第一个数的下标,第三层遍历,算法复杂度 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-1−1 的个数相同)。
进一步,总和为 000 意味着我们可以用前缀和来维护一段区间的和。设前缀和数组为 sss ,bbb (亦即 a[i]=0a[i] =0a[i]=0)所在位置为 ppp ,那么对于任意一段区间 [[[ lll ,,, rrr ]]] ,只要满足 s[l−1]==s[r]s[l-1]==s[r]s[l−1]==s[r] ,且 l−1≤pl-1\le pl−1≤p && p≤rp\le rp≤r,那么 [[[ lll ,,, rrr ]]] 就是一段合法子序列。
于是,问题转化成:求 s0s_0s0 ~ sp−1s_{p-1}sp−1 与 sps_psp ~ sns_nsn 两组数中相同的数字对数。
那么这个问题如何解决呢?注意到 sss 数组中的数都属于 [−n,n][-n,n][−n,n] ,因此不妨打个哈希表存储每个数字在 s0s_0s0 ~ sp−1s_{p-1}sp−1 与 sps_psp ~ sns_nsn 两组数中各出现了多少次,最后结果就是:
∑i=−nnvh[i][0]∗vh[i][1]\sum_{i=-n}^n vh[i][0]*vh[i][1]i=−n∑nvh[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;
}
本文讨论了如何将一个关于计算排列中中位数子序列的问题,从暴力的O(n^3)复杂度优化到高效的O(n)复杂度。首先介绍了暴力枚举和前缀和优化的方法,然后揭示了一个巧妙的算法,通过修改数组并利用前缀和统计满足条件的子序列。
243

被折叠的 条评论
为什么被折叠?



