题目描述
小蓝的班上有 n 个人,一次考试之后小蓝想统计同学们的成绩,第 i 名同学的成绩为 ai 。当小蓝统计完前 x 名同学的成绩后,他可以从 1 ∼ x 中选出任意 k 名同学的成绩,计算出这 k 个成绩的方差。小蓝至少要检查多少个人的成绩,才有可能选出 k 名同学,他们的方差小于一个给定的值 T ?
提示:k 个数 v1, v2, · · · , vk 的方差 定义为:
,其中
表示 v 的平均值,
。
输入格式
输入的第一行包含三个正整数 n, k, T ,相邻整数之间使用一个空格分隔。
第二行包含 n 个正整数 a1, a2, · · · , an ,相邻整数之间使用一个空格分隔。
输出格式
输出一行包含一个整数表示答案。如果不能满足条件,输出 −1 。
样例输入
5 3 1
3 2 5 2 3
样例输出
4
提示
【样例说明】
检查完前三名同学的成绩后,只能选出 3, 2, 5 ,方差为 1.56 ;检查完前四名同学的成绩后,可以选出 3, 2, 2 ,方差为 0.33 < 1 ,所以答案为 4 。
【评测用例规模与约定】
对于 10% 的评测用例,保证 1 ≤ n, k ≤ 10^2;
对于 30% 的评测用例,保证 1 ≤ n, k ≤ 10^3 ;
对于所有评测用例,保证 1 ≤ n, k ≤ 10^5 ,1 ≤ T ≤ 2^31 − 1 ,1 ≤ ai ≤ n 。
整体思路
考虑二分查找答案位置,对于前 mid 个数,先将其从小到大排序,每次从中连续地取出 k 个数,然后计算其方差。
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
int k, T;
bool check(int loc, const vector<int>& a)
{
vector<int> b(loc + 1);
copy(a.begin() + 1, a.begin() + 1 + loc, b.begin() + 1);
sort(b.begin() + 1, b.begin() + 1 + loc);
int sum = 0, sum_sq = 0; // 一次项和二次项
double avev; // 一次项平均数
for(int i = 1; i < k; i++)
{
sum += b[i];
sum_sq += b[i] * b[i];
}
for(int i = k; i <= loc; i++)
{
sum -= b[i - k];
sum += b[i];
sum_sq -= b[i - k] * b[i - k];
sum_sq += b[i] * b[i];
avev = 1.0 * sum / k;
if((sum_sq - 2 * avev * sum + k * avev * avev) / k <= T)
{
return true;
}
}
return false;
}
signed main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
cin >> n >> k >> T;
vector<int> a(n + 1);
for(int i = 1; i <= n; i++)
{
cin >> a[i];
}
if(check(n, a) == false)
{
cout << -1 << endl;
exit(0);
}
int left = k, right = n, ans;
while(left <= right)
{
int mid = left + ((right - left) >> 1);
if(check(mid, a) == true)
{
ans = mid;
right = mid - 1;
}
else
{
left = mid + 1;
}
}
cout << ans << endl;
return 0;
}
公式推导
其中 vi 的和以及平方和可以用队列的形式维护,平均值则可以通过已知条件计算得到,这样就能用线性的时间复杂度快速求出方差了。
具体步骤
1. 先将数组 a 的前 loc 拷贝到数组 b 中,之后进行排序(防止数组 a 内数据变化)。
2. 计算出 vi 前 k 项的和以及平方和,利用滑动窗口的思想保持窗口长度为 k 向后遍历,分别计算出 k 项的方差,判断是否满足题意。
3. 利用二分法搜索答案位置,注意边界条件的选取:我们所求答案 target 是在一个左闭右闭的区间里,也就是 [left, right] 。因为 left == right 是有意义的,所以 while (left <= right) 时要使用 <= 。因为当前这个 mid 一定不是 target,那么接下来要查找的左区间结束下标位置就是 mid - 1,所以right 要赋值为 mid - 1,left 同理为 mid + 1。