1. 题目解析与算法思路
这道题目要求我们统计所有连续子序列中,中位数大于等于给定值X的子序列数量。首先我们需要明确几个关键概念:
1.1 中位数定义
题目中对中位数的定义是:对于一个长度为K的子序列,排序后取第ceil(K/2)个元素。例如:
- {7,3,2,6}排序后为{2,3,6,7},ceil(4/2)=2,中位数为6
- {5,4,8}排序后为{4,5,8},ceil(3/2)=2,中位数为5
1.2 关键观察
判断一个子序列的中位数是否≥X,等价于判断该子序列中≥X的元素数量是否超过<X的元素数量。例如:
- 子序列{10,5,6}:≥6的有2个(10,6),<6的有1个(5),满足条件
- 子序列{5,6,2}:≥6的有1个(6),<6的有2个(5,2),不满足
1.3 算法转换
基于这个观察,我们可以将问题转换为:
- 将原数组转换:≥X的元素记为+1,<X的元素记为-1
- 计算前缀和数组pre[i] = sum(转换后数组的前i个元素)
- 子序列a[j...i]满足条件等价于pre[i]-pre[j-1] ≥ 0
- 问题转化为统计i>j时pre[i]≥pre[j]的对数
2. 树状数组优化实现
2.1 前缀和数组处理
我们首先处理输入数据,构建转换后的前缀和数组:
cpp复制for(int i = 1;i <= n;i++){
scanf("%lld",&a);
if(a >= x)
pre[i] = pre[i - 1] + 1;
else
pre[i] = pre[i - 1] - 1;
}
这里pre[i]表示前i个元素转换后(+1/-1)的和。
2.2 树状数组应用
为了高效统计满足pre[i]≥pre[j]的(i,j)对数,我们使用树状数组:
cpp复制add(n + 1,1); // 初始化,处理pre[0]=0的情况
for(int i = 1;i <= n;i++){
ans += sum(pre[i] + n + 1); // 查询≤pre[i]的数量
add(pre[i] + n + 1,1); // 将当前pre[i]加入树状数组
}
这里有几个关键点:
- pre[i]可能为负数,所以我们加上n+1偏移量使其变为正数
- sum(x)查询的是≤x的pre[j]出现次数
- 每次处理完pre[i]后将其加入树状数组
2.3 时间复杂度分析
- 预处理:O(n)
- 树状数组操作:每次add和sum都是O(log n)
- 总时间复杂度:O(n log n)
3. 完整代码解析
cpp复制#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
ll n,x,ans;
ll a,pre[200005],c[200005];
int lowbit(int x){
return x & -x;
}
void add(int x,int k){
while(x <= n * 2 + 1){
c[x] += k;
x += lowbit(x);
}
}
int sum(int x){
int cnt = 0;
while(x >= 1){
cnt += c[x];
x -= lowbit(x);
}
return cnt;
}
int main(){
scanf("%lld%lld",&n,&x);
for(int i = 1;i <= n;i++){
scanf("%lld",&a);
if(a >= x)
pre[i] = pre[i - 1] + 1;
else
pre[i] = pre[i - 1] - 1;
}
add(n + 1,1); // pre[0] = 0
for(int i = 1;i <= n;i++){
ans += sum(pre[i] + n + 1);
add(pre[i] + n + 1,1);
}
printf("%lld\n",ans);
return 0;
}
3.1 代码细节说明
- 使用long long防止溢出,因为结果可能很大
- 数组大小开2*10^5,考虑n的最大值和偏移量
- lowbit、add、sum是树状数组的标准操作
- 初始化add(n+1,1)对应pre[0]=0的情况
4. 示例解析
以题目中的示例为例:
输入:
code复制4 6
10
5
6
2
处理过程:
- 转换为+1/-1数组:[1, -1, 1, -1]
- 前缀和pre:[0, 1, 0, 1, 0]
- 统计过程:
- pre[1]=1:sum(1+n+1)=sum(6)=1 (pre[0]=0)
- pre[2]=0:sum(0+n+1)=sum(5)=1
- pre[3]=1:sum(1+n+1)=sum(6)=2
- pre[4]=0:sum(0+n+1)=sum(5)=2
- 累计ans=1+1+2+2=6,加上空子序列共7个
5. 算法优化与变种
5.1 其他解法对比
- 暴力解法:枚举所有子序列,排序求中位数。时间复杂度O(n^3),无法通过
- 部分优化:维护有序结构,时间复杂度O(n^2),对于n=1e5仍然不够
- 树状数组/线段树:O(n log n),是本题最优解
5.2 类似问题
- 统计中位数≤X的子序列数量
- 统计平均数≥X的子序列数量
- 统计众数满足某种条件的子序列数量
6. 常见错误与调试技巧
6.1 常见错误
- 数组大小不足:pre和c数组需要开2*n大小
- 整数溢出:ans可能很大,要用long long
- 偏移量处理不当:pre[i]可能为负,需要加n+1偏移
- 初始化遗漏:忘记处理pre[0]=0的情况
6.2 调试建议
- 打印转换后的数组和前缀和数组
- 小规模数据手动计算验证
- 检查树状数组的查询和更新操作
提示:在竞赛中遇到这类问题时,先考虑小规模数据的暴力解法,确保理解题意后再寻找优化方法。
7. 实际应用与扩展
这种将中位数问题转换为前缀和统计的技巧,在实际中有广泛应用:
- 金融数据分析:统计股价高于某阈值的时间段
- 生物信息学:分析基因序列中特定模式的出现
- 质量控制:监控生产线上产品参数达标情况
对于想要进一步学习的同学,可以研究:
- 二维情况下的类似问题
- 带修改操作的中位数查询
- 结合其他统计量的复合查询
8. 编程竞赛技巧
在解决这类算法竞赛题目时,我总结了一些实用技巧:
- 问题转换是关键:将不直观的中位数条件转换为可计算的前缀和条件
- 数据结构选择:树状数组适合单点更新和前缀查询,比线段树更简洁
- 边界处理:特别注意数组下标和偏移量的处理
- 数据类型:大规模数据时注意使用long long防止溢出
在实际编码时,建议先写出伪代码,明确每个步骤的逻辑,然后再实现细节。例如本题的步骤:
- 读取输入并转换
- 计算前缀和
- 初始化树状数组
- 遍历统计结果
- 输出答案
这种分步实现的思路可以降低编码复杂度,提高正确率。