原题链接:109. 天才ACM
解题思路
首先,对于一个集合S,显然应该取S中最大的M个数和最小的M个数,最大和最小构成一对,次大和次小构成一对···这样求出“校验值”最大。而为了让数列A分成的段数最少,每一段都应该在“校验值”不超过T的前提下,尽量包含更多的数。所以我们从头开始对A进行分段,让每一段尽量长,到达结尾时分成的段数就是答案。
于是,需要解决的问题为:当确定一个左端点L之后,右端点R在满足A[L]~A[R]的“校验值”不超过T的前提下,最大能取到多少。
求长度为N的一段“校验值”需要配对,时间复杂度为O(NlogN)。当“校验值”上限T比较小时,如果在整个L~N的区间上二分右端点R,二分的第一步就要检验(N-L)/2这么长一段,最终右端点R却可能只扩展了一点,浪费了很多时间。与上一道题目一样,我们需要一个与右端点R扩展的长度相适应的算法----倍增。
可以采用与上一题类似的倍增过程:
1.初始化 p=1,R=L。
2.求出[L,R+p]这一段区间内的“校验值”<=T,则R+=p,p*=2,否则p/=2.
3.重复上一步,直到p的值变为0,此时R即为所求。
样例代码
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 500005;
int n, m;
int ans;
ll T;
ll w[N], t[N];
ll sq(ll x)
{
return x * x;
}
ll get(int l, int r) // 计算原数组左闭右开区间 [l, r) 的校验值
{
int k = 0;
for (int i = l; i < r; i ++ )
t[k ++ ] = w[i];
sort(t, t + k);
ll sum = 0;
for (int i = 0; i < m && i < k; i ++ , k -- )
sum += sq(t[i] - t[k - 1]);
return sum;
}
int main()
{
int K;
scanf("%d", &K);
while (K -- )
{
scanf("%d %d %lld
", &n, &m, &T); // 不用 scanf 的话,这么做有一个点会 TLE
for (int i = 0; i < n; i ++ )
scanf("%lld", &w[i]);
ans = 0;
int start = 0, end = 0; // start 记录剩余区间开头节点,end 记录当前考虑区间的尾结点(左闭右开)
while (end < n)
{
int len = 1; // len 初始化为 1
while (len) // len 为 0 自动跳出
{
if (end + len <= n && get(start, end + len) <= T) // 如果说 len + end 还在 n 以内,且区间 [start, end + len) 的校验值不大于 T
end += len, len <<= 1; // 那么 end += len,len *= 2
else len >>= 1; // 否则 len /= 2
}
start = end; // 让 start 指向当前区间末尾结点的下一个位置,由于区间是左闭右开的,所以直接指向 end 就可以了
ans ++ ; // 每次循环都找到了一个区间,所以让 ans ++
}
printf("%d
", ans);
}
return 0;
}
// 偷个懒,少写点注释 (  ̄▽ ̄)σ