【树状数组 二分】中位数之中位数
题意
给出一个长度为\(n\)的序列\(a\),首先求出其所有区间的中位数,将这些中位数构成的集合记为\(S\),求\(S\)中所有数的中位数。
此题中位数指:有\(n\)个数,第\(\left \lfloor \frac{n}{2} \right \rfloor+1\)个数即为中位数。
数据范围:\(1\leq n\leq 10^5,1\leq a_i\leq 10^9\)。
思路
因为求的是\(S\)的中位数,发现答案具有单调性,考虑二分。
二分答案记为\(x\),判断它是否是\(S\)的中位数。那就要看区间中位数\(\leq x\)的有多少个,从而确定这个数在集合\(S\)中的位置,记为\(res\)。
发现如果枚举的时间复杂度高,可以转换一下方式,考虑判断单个区间\([l,r]\)的中位数与\(x\)的大小关系,再看有多少个满足条件的区间。
设\(p_i\)为\([1,i]\)中\(>x\)数的个数。如果区间\([l,r]\)的中位数\(\leq x\),可以得到\(r-l+1>2*(p_r-p_{l-1})\)(即\(>x\)的个数没有达到区间个数的一半)。
然后看这个判断的式子,可以变成\(r-(l-1)>2*(p_r-p_{l-1})\),移项变成\(r-2*p_r>(l-1)-2*p_{l-1}\)。
发现对于一个区间右端点\(r\),满足这个式子的\(l-1\)的个数即为\(res\)。可用树状数组维护这个答案。
然后就是根据\(res\)进行答案区间的缩小。记实际中位数的位置为\(w\),因为求的\(res\)是\(\leq x\)的个数(即是一段相同的\(x\)的最后一个,如122333中2的res是3,3的res是6),所以满足\(x\)的范围包括了\(w\),即要使\(res\)是\(w\)的后继(第一个\(\geq w\)的数),所以二分求的是这么一个东西。
一些细节:\(S\)的区间长度会超出\(int\)。
对于\(i-2*p_i\)这个东西的范围,是\(-n\sim n\)的,所以下标都加上\(n\)
二分的细节,比较多,建议重新看回蓝书。详见代码。
代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#define int long long
int n, m;
int a[100001], b[100001], p[100001], t[200001];
void add(int x) {
for (; x <= 2 * n; x += x & -x)
t[x]++;
}
int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += t[x];
return res;
}
int check(int x) {
int tot = 0;
memset(t, 0, sizeof(t));
add(n);
for (int i = 1; i <= n; i++) {
p[i] = p[i - 1] + 2 * (a[i] > x);
add(i - p[i] + n);
tot += query(i - p[i] + n - 1);
}
return tot < m / 2 + 1;
}
signed main() {
scanf("%lld", &n);
m = n * (n + 1) / 2;
for (int i = 1; i <= n; i++)
scanf("%lld", &a[i]), b[i] = a[i];
std::sort(b + 1, b + n + 1);
int tot = std::unique(b + 1, b + n + 1) - (b + 1);
int l = 1, r = tot;
while (l < r) {
int mid = l + r >> 1;
if (check(b[mid])) l = mid + 1;//求后继,那么在check之后,发现mid不满足,所以我们可以令l为mid+1
else r = mid;//发现mid满足,试图求出最小的,所以mid这个答案要先保留,所以令r=mid
}
printf("%lld", b[l]);
}