..
AcWing 102. 最佳牛围栏
二分答案,判定 “是否存在一个长度不小于 $L$ 的子段,平均数不小于二分的值”。
如果把数列中每个数都减去二分的值,就转化为判定 “是否存在一个长度不小于 $L$ 的子段,子段和非负”。
-
求一个子段,它的和最大,没有 “长度不小于 $L$” 这个限制。
无长度限制的最大子段和问题是一个经典问题,只需 $\mathcal{O}(n)$ 扫描该数列,不断把新的数加入子段,当子段和变成负数时,把当前的整个子段清空。
扫描过程中出现过的最大子段和即为所求。 -
求一个子段,它的和最大,子段的长度不小于 $L$。
子段和可以转化为前缀和相减的形式,即设 $sum_i$ 表示 $A_1 \sim A_i$ 的和,则有:
$\max_{i - j \geq L} { A_{j + 1} + A_{j + 2} + \cdots + A_i } = \max_{L \leq i \leq n} { sum_i - \min_{0 \leq j \leq i - L} { sum_j } }$
随着 $i$ 的增长,$j$ 的取值范围 $0 \sim i - L$ 每次只会增大 $1$。
换言之,每次只会有一个新的取值进入 $\min { sum_j }$ 的候选集合,所以我们没有必要循环枚举 $j$,只需要用一个变量记录当前最小值,每次与新的取值 $sum_{i - L}$ 取 $\min$ 就可以了。
double ans = -1e10;
double min_val = 1e10;
for (int i = L; i <= N; i++) {
min_val = min(min_val, sum[i - L]);
ans = max(ans, sum[i] - min_val);
}
解决了问题 2 之后,我们只需要看一下最大子段和是不是非负数,就可以确定上下界的变化范围了。
double a[100001], b[100001], sum[100001];
int main() {
int N, L; cin >> N >> L;
for (int i = 1; i <= N; i++) scanf("%lf", &a[i]);
double eps = 1e-5;
double l = -1e6, r = 1e6;
while (r - l > eps) {
double mid = (l + r) / 2;
for (int i = 1; i <= N; i++) b[i] = a[i] - mid;
for (int i = 1; i <= N; i++)
sum[i] = (sum[i - 1] + b[i]);
double ans = -1e10;
double min_val = 1e10;
for (int i = L; i <= N; i++) {
min_val = min(min_val, sum[i - L]);
ans = max(ans, sum[i] - min_val);
}
if (ans >= 0) l = mid; else r = mid;
}
cout << int(r * 1000) << endl;
}