【树状数组 二分】中位数之中位数

题意

给出一个长度为\(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]);
}
posted @ 2020-11-30 11:41  nymph181  阅读(659)  评论(0)    收藏  举报