最佳牛围栏(二分+前缀和)
题意:
给定n个[1,2000]的数,可以选取一段长度 >= F的连续子段和,让你找到一个平均值最大的子段,输出这个最大的平均值。
题解:
二分平均值:假设当前二分的平均值为mid,那么在序列中寻找是否有平均值>=mid 且 l e n > = F len >= F len>=F的子段。我们可以让序列每个数减去mid,则问题转换为在序列中寻找一个 l e n > = F len >= F len>=F 且 子段和为非负的子段。
可以用前缀和预处理出减去mid后的序列的前缀和。
设前缀和数组为sum,则我们要找到一个子段
[
i
,
j
]
(
j
−
i
>
=
F
)
,
s
u
m
[
j
]
−
s
u
m
[
i
]
>
=
0
[i,j](j - i >= F),sum[j] - sum[i]>=0
[i,j](j−i>=F),sum[j]−sum[i]>=0,
则说明当前mid可行,设置
l
=
m
i
d
l=mid
l=mid继续判断更大的平均值.,否则设置
r
=
m
i
d
r=mid
r=mid缩小平均值。
由于数据是 1 0 5 10^5 105级别,显然通过枚举 i , j i,j i,j的做法会超时 ( O ( n 2 ) ) (O(n^2)) (O(n2))。
事实上我们可以做如下转换:
∃
(
s
u
m
[
j
]
≥
s
u
m
[
i
]
)
⇒
s
u
m
[
j
]
≥
m
i
n
{
s
u
m
[
k
]
,
0
≤
k
≤
i
}
\exists( sum[j] \ge sum[i]) \Rightarrow sum[j] \ge min\{sum[k], 0 \le k \le i\}
∃(sum[j]≥sum[i])⇒sum[j]≥min{sum[k],0≤k≤i}
利用双指针
i
,
j
i,j
i,j便可以在
O
(
n
)
O(n)
O(n)内检查是否有满足条件的连续序列,
初始时
i
=
0
,
j
=
F
i = 0, j = F
i=0,j=F,并设
m
i
n
V
=
m
i
n
{
s
u
m
[
k
]
,
0
≤
k
≤
i
}
minV = min\{sum[k], 0 \le k \le i\}
minV=min{sum[k],0≤k≤i},每次迭代时更新
m
i
n
V
minV
minV并判断当前子段是否非负,如果不是则同时后移
i
,
j
i,j
i,j继续判断。
显然二分边界为
l
=
0
,
r
=
m
a
x
(
c
o
w
[
i
]
)
l = 0,r = max(cow[i])
l=0,r=max(cow[i])。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n,f;
int cow[N];
double sum[N];
bool check(double avg)
{
//预处理前缀和数组
for(int i = 1; i <= n; i++)
sum[i] = sum[i-1] + cow[i] - avg;
double minv = 0; // 记录sum[0..i]中最小值
for(int i = 0, j = f; j <= n; j++,i++)
{
minv = min(minv,sum[i]);
// 如果满足,则说明存在一段非负和的子段
if(sum[j] >= minv) return true;
}
return false;
}
int main()
{
scanf("%d%d",&n,&f);
double l = 0, r = 0;
for(int i = 1 ; i <= n; i++)
{
scanf("%d",cow + i);
r = max(r, (double)cow[i]);
}
while(r - l > 1e-5)
{
double mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
printf("%d", (int)(r * 1000));
return 0;
}