洛谷 P1627 [CQOI2009] 中位数
Description
给出 \(1,2,...,n\) 的一个排列,统计该排列有多少个长度为奇数的连续子序列(子段)的中位数是 \(b\)。中位数是指把所有元素从小到大排列后,位于中间的数。
Constraints
对于 \(30\%\) 的数据中,满足 \(n \le 100\);
对于 \(60\%\) 的数据中,满足 \(n \le 1000\);
对于 \(100\%\) 的数据中,满足 \(n \le 100000\) , \(1 \le b \le n\)。
Solution
序列长度必为奇数,而排列中只有一个 \(b\),所以中位数为 \(b\) 的子段里肯定包含 \(b\)。
又由于中位数只是关心相对位置,而不关心数的具体大小,可以把 \(\lt b\) 的所有数变为 \(-1\),把 \(gt b\) 的所有数变为 \(-1\),\(b\) 变成 \(0\)。
问题便转化为在转换后数组中有多少带 \(b\) 且总和为 \(0\) 的子段了。
我们可以考虑从 \(b\) 开始,向数组两边跑前缀/后缀和,然后分类累计答案:
-
只延伸左边区间,则为左边到 \(b\) 后缀和为 \(0\) 的个数;
-
只延伸右边区间,则为右边以 \(b\) 开始的前缀和为 \(0\) 的个数;
-
左右区间均要延伸,设现在找到的左边位置前缀和为 \(i(i≠0)\) 有 \(l[i]\) 个位置, 右边位置前缀和为 \(-i\) 的有 $r[i] 个,则根据组合计算可知,答案应累加 \(l[i] \times r[i]\),对于每一个 \(i\) 加一次即可。
最终总答案便是三类答案之和。
注意一个细节:因为前缀/后缀和可能出现负数,所以前后缀和数组下标要进行处理(如加一个大数)。
Code:
// by youyou2007 in 2022.
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <queue>
#include <stack>
#include <map>
#define REP(i, x, y) for(int i = x; i < y; i++)
#define rep(i, x, y) for(int i = x; i <= y; i++)
#define PER(i, x, y) for(int i = x; i > y; i--)
#define per(i, x, y) for(int i = x; i >= y; i--)
#define lc (k << 1)
#define rc (k << 1 | 1)
using namespace std;
const int N = 5E5 + 5;
const int TEMP = 2E5;
int n, b;
int a[N], f[N];
int l[N], r[N], pos, ans = 1;
int main()
{
scanf("%d%d", &n, &b);
rep(i, 1, n)
{
scanf("%d", &a[i]);
if(a[i] > b) a[i] = 1;//转换数组
else if(a[i] == b) pos = i;
else a[i] = -1;
}
rep(i, pos + 1, n)
{
f[i] = f[i - 1] + a[i];//前缀和,由于与后缀和位置不影响,可以合并成一个数组操作
if(f[i] == 0) ans++;//处理第一种情况
l[f[i] + TEMP]++;//l数组用来统计和的每个值出现的次数,便于组合计算
}
per(i, pos - 1, 1)
{
f[i] = f[i + 1] + a[i];//后缀和同理
if(f[i] == 0) ans++;//处理第二种情况
r[f[i] + TEMP]++;
}
rep(i, TEMP - n, TEMP + n)
{
ans += l[i] * r[TEMP - (i - TEMP)];//处理第三种情况
}
printf("%d", ans);
return 0;
}