【洛谷】P1404 平均数
题目描述
给一个长度为 n n n 的数列,我们需要找出该数列的一个子串,使得子串平均数最大化,并且子串长度 ≥ m \ge m ≥m。
输入格式
第一行两个整数 n n n 和 m m m。
接下来 n n n 行,每行一个整数 a i a_i ai,表示序列第 i i i 个数字。
输出格式
一个整数,表示最大平均数的 1000 1000 1000 倍,如果末尾有小数,直接舍去,不要用四舍五入求整。
样例输入 #1
10 6
6
4
2
10
3
8
5
9
4
1样例输出 #1
6500提示
- 对于 60 % 60\% 60% 的数据,保证 m ≤ n ≤ 1 0 4 m\le n\le 10^4 m≤n≤104;
- 对于 100 % 100\% 100% 的数据,保证 1 ≤ m ≤ n ≤ 1 0 5 1 \leq m\le n\le 10^5 1≤m≤n≤105, 0 ≤ a i ≤ 2000 0\le a_i\le2000 0≤ai≤2000。
题解
题目的要求可细分为两点,一是子序列的均值最大,二是子序列的长度不低于m。在求解时,我们能想到的最直接的方法就是用两重循环来一一枚举该序列的所有子序列,并分别求其均值以寻找最大均值(这里又是一重循环),因此暴力枚举方法的时间复杂度为
O
(
n
3
)
O\left(n^3\right)
O(n3)。这在题目给出的数据范围下,必定超时无疑。这时候,可能你会马上想到一种数据结构——前缀和
(尚不清楚这一数据结构的童鞋请移步学习 ☞ 传送门 ),它的存在可将子段和转化为前缀和相减的形式。但不幸的是,其也只能将上面思路中求每个子序列均值的那重循环降至常数级。因此,采取这样的方法求解的事件复杂度为
O
(
n
2
)
O\left(n^2\right)
O(n2) ,依然不能拿到满分。
这时候,我们需要另辟奚径地来思考这道题。题目不是要求序列中的最大均值么?我们可以将“查找具有最大均值的子序列”转换为“对于某个值,是否存在某个子序列的均值比它更大”。这一转换非同小可,因为前者(在一个序列中枚举所有子序列)是一个 O ( n 2 ) O\left(n^2\right) O(n2) 的问题,而后者(在一个范围内寻找某个值)是一个 O ( n ) O\left(n\right) O(n) 的问题。更进一步地,对于这种区间寻值的问题,我们似乎很容易想到一种算法思想——分治。于是,我猜你也想到了,二分法。
基于以上分析,可大致将求解思路总结如下:
- 确定题目所给数列的均值范围。由于题目给出的数据非负,因此其输入的数列均值最小为 0。对于最大值,可以直接用题目给出的数据范围,也可以用一个寄存器从输入数据中记录输入数列的最大值,并以该值作为此数列的均值上限;
- 对1中得到均值上下限,采用二分法进行查找(枚举),每次都对当前的中间值 m i d a v e midave midave 进行判断,判断内容为:当前数列里是否存在一个长度不低于m的子序列,其均值不低于 m i d a v e midave midave?如果是,则将二分法查找的下限置为 m i d a v e midave midave;否则将二分法查找的上限置为 m i d a v e midave midave。
最后程序结束时,再将得到的均值乘以 1000 即可。
接下来,我们的问题就变为:如何判断数列里是否存在一个长度不低于 m m m 的子序列,其均值不低于 m i d a v e midave midave?注意到一件事,这里依然需要查找子序列。为了避免重复计算,便考虑构建前缀和数组。但是,我们还要像最开始提到的方式来一一枚举子序列么?如果是,那现在做的思路转换其实并无意义(这就好比你把难的题放到最后做,最终还是逃不掉面对它的那一刻)。如果不是,那要如何求解呢?
再对当前的情况进行一个梳理,现在我们的任务是:对于一个输入的指定值 m i d a v e midave midave,判断给定序列中是否存在长度不低于 m m m 的子序列,其均值大于 m i d a v e midave midave。对于这个问题我们可以做这样的处理:将原序列中的每个数值都减去该均值,对处理后的序列而言,一旦出现某段子序列的总和大于0,就说明该序列对应在原序列部分的元素均值是大于 m i d a v e midave midave的。
因此,现在我们在拿到 m i d a v e midave midave 时,可对原数列( a r y [ ] ary[\ ] ary[ ])做如下两步处理:
- 构建原数列减去 m i d a v e midave midave 后的数列,即: a r y G a p [ i ] = a r y [ i ] − m i d a v e aryGap[i] = ary[i]-midave aryGap[i]=ary[i]−midave;
- 基于新的差值数组构建前缀和数组,即: p r e f i x [ i ] = p r e f i x [ i − 1 ] + a r g [ G a p ] prefix[i] = prefix[i-1]+arg[Gap] prefix[i]=prefix[i−1]+arg[Gap]。
这样,当我们需要判断某段序列(子区间)的均值是否大于 m i d a v e midave midave 时,实际上就只需要判断当前区间的和是否大于 0。对前缀和数组而言,其仅需要做一次运算和比较,即 p r e f i x [ j ] − p r e f i x [ i ] prefix[j]-prefix[i] prefix[j]−prefix[i] 是否大于 0 即可(这里对应的子区间为 [ i + 1 , j ] [i+1,j] [i+1,j] )。于是,这就将问题转换为:判断前缀和序列里,是否存在一个长度不低于 m m m 的间隔,使得位于后面的取值不低于前面。而这个问题简单太多,用一次循环扫描就能得出结论(在循环中记录当前的最小前缀和 m i n G a p minGap minGap,然后每次扫描到一个元素时就判断当前元素的取值是否不低于前面记录的,是则代表目前存在一个满足要求的子序列,否则不存在)。
下面给出基于以上思路得到的完整代码:
/*
【洛谷】 P1404
*/
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e5+5;
int ary[MAX];
double prefix[MAX],eps=1e-8;
// 此函数用于判断:原数组中是否存在长度不低于 m 的子序列,使得其均值大于 thresold
bool anySeq(int ary[], int n, int m, double thresold){
double minGap = 0;
// 计算以 thresold 为基准值得到的差值数组的前缀和数组
for(int i=1;i<=n;i++){
prefix[i] = prefix[i-1] + (ary[i]-thresold);
// 当 i 不低于 m 时,就能检测子序列的存在情况了
if(i>=m) {
minGap = min(minGap, prefix[i-m]);
// 只要该前缀和数组中存在不低于 0 的值,说明一定存在某个序列能使其均值大于 thresold
if(prefix[i]>minGap) return true;
}
}
return false;
}
int main( )
{
// 录入数据
int n,m,maxtmp=-1; cin>>n>>m;
// 录入原始数组,并记录最大值(该最大值表明此数组能取到的最大平均值即为该值)
for(int i=1;i<=n;i++){
cin>>ary[i];
maxtmp = max(maxtmp, ary[i]);
}
// 对最大平均值进行二分查找
double minave = 0, maxave = maxtmp, midave;
// 这里必须用精度进行控制,否则会超时
while(maxave - minave >= eps){
// 设置当前的均值
midave = (minave+maxave) / 2;
// 判断当前数组是否存在能大于该平均值的序列(长度不低于 m)
if(anySeq(ary, n, m, midave)) minave = midave;
else maxave = midave;
}
// 进行放大后再输出
cout<<int(maxave*1000)<<endl;
return 0;
}