算法知识图谱总览 / 离线与杂项技巧族 / 最大平均值区段

最大平均值区段

P2advancedstandardG-E-7

What · 是什么

求长度≥L的最大平均值子数组:二分+前缀和判定,分数规划经典应用

最大平均值区段问题:给数组 a[1..n] 与下界 L,求长度不小于 L 的连续子段使平均值最大。经典解法:二分答案 x,把判定转化为检查是否存在长度≥L 且 sum-a*x 非负的子段(即 max subarray sum 限制长度)。等价于分数规划(最大化 sum(A)/len(A) 的约束版)。可用前缀和+单调队列 O(n) 判定,二分后总复杂度 O(n log precision)。

Why · 为什么

直觉 · Intuition

直接枚举所有长度≥L 子段 O(n²) 不可行。观察'平均值≥x'等价于'子段每个数减去 x 后和≥0',于是二分 x 后问题变成找最大子段和(带长度下界)。该子段和可通过前缀和最小化 O(n) 求出(当前 r 处最大和 = pref[r] - min(pref[0..r-L]))。

原理 · Principle

判定(x):构造 b[i]=a[i]-x,前缀和 pref。对右端点 r,长度≥L 的最大子段和为 pref[r]-min(pref[0..r-L]),只需维护窗口内前缀和最小值。若存在≥0,说明存在子段平均值≥x,提高下界;否则降低。二分迭代至精度满足(通常 1e-6)。该技巧是分数规划(0/1分数规划二分)的基础形态。

历史 · History

问题本质是'最大子段和'的约束长度版本。CP-Algorithms 的最大/最小子段和页给出前缀和+维护当前最小值的 O(n) 求最大子段和算法,并延伸到限制长度(L 下界)需用单调队列/双端队列维护窗口内前缀和极值。这是二分答案结合判定函数的典型题,POJ 上有多道同名经典题。

How · 实现

伪代码

# 二分答案求最大平均值(长度>=L)
lo, hi = min(a), max(a)
for _ in range(60):
  mid = (lo + hi) / 2
  if feasible(mid): lo = mid
  else: hi = mid
return lo

def feasible(x):
  pref[0] = 0
  for i in 1..n: pref[i] = pref[i-1] + a[i] - x
  min_pref = inf
  for r in L..n:
    min_pref = min(min_pref, pref[r - L])
    if pref[r] - min_pref >= 0: return True
  return False

Python 实现

def max_avg_segment(a, L):
    """长度>=L 的最大平均值子段。二分+前缀和判定 O(n log)。"""
    n = len(a)

    def feasible(x):
        pref = 0.0
        # 维护窗口 [0..r-L] 内前缀和最小值
        # 用前缀和数组便于取 pref[r-L]
        pref_list = [0.0] * (n + 1)
        for i in range(n):
            pref_list[i + 1] = pref_list[i] + a[i] - x
        min_pref = float('inf')
        for r in range(L, n + 1):
            # 可用左端点 l 属于 [1, r-L+1], 对应 pref[l-1] 最小
            min_pref = min(min_pref, pref_list[r - L])
            if pref_list[r] - min_pref >= 0:
                return True
        return False

    lo, hi = min(a), max(a)
    for _ in range(60):  # 足够精度
        mid = (lo + hi) / 2.0
        if feasible(mid):
            lo = mid
        else:
            hi = mid
    return lo


def max_avg_segment_brute(a, L):
    """O(n^2) 暴力用于对拍。"""
    best = float('-inf')
    n = len(a)
    for i in range(n):
        s = 0.0
        for j in range(i, n):
            s += a[j]
            if j - i + 1 >= L:
                best = max(best, s / (j - i + 1))
    return best


def max_subarray_sum_kadane(a):
    """Kadane: 最大子段和(不限长度)。"""
    cur = best = a[0]
    for x in a[1:]:
        cur = max(x, cur + x)
        best = max(best, cur)
    return best


if __name__ == '__main__':
    import random
    random.seed(3)
    # 确定性样例
    a = [1, 4, 7, 2, 5, 8]
    assert abs(max_avg_segment(a, 2) - 6.5) < 1e-9  # 4+7+2+5+8 = 26/4? 不对
    # 实际最优: 取 [7,2,5,8]=22/4=5.5? 取 [4,7,2,5,8]=26/5=5.2 ... 最优是 [7,2,5,8]=5.5
    # 或 [1,4,7,2,5,8] 全段 27/6=4.5; [4,7]=5.5; [5,8]=6.5 -> 最优 [5,8]=6.5
    assert abs(max_avg_segment(a, 2) - 6.5) < 1e-9
    assert abs(max_avg_segment_brute(a, 2) - 6.5) < 1e-9

    assert max_subarray_sum_kadane([-2, 1, -3, 4, -1, 2, 1, -5, 4]) == 6
    assert max_subarray_sum_kadane([5, 4, -1, 7, 8]) == 23

    # 对拍
    for _ in range(100):
        n = random.randint(1, 12)
        arr = [random.randint(-10, 10) for _ in range(n)]
        L = random.randint(1, n)
        r1 = max_avg_segment(arr, L)
        r2 = max_avg_segment_brute(arr, L)
        assert abs(r1 - r2) < 1e-6, (arr, L, r1, r2)

    print('max_avg a,L=2 =', max_avg_segment(a, 2))
    print('kadane [-2,1,-3,4,-1,2,1,-5,4] =', max_subarray_sum_kadane([-2, 1, -3, 4, -1, 2, 1, -5, 4]))
    print('random brute-compare (100 cases): OK')
    print('All max-avg-segment tests passed!')
PyPy 提示二分 60 轮每轮 O(n), n=1e5 时 PyPy 可行; 判定中可用 deque 维护窗口前缀和最小值代替列表重算; 浮点比较用绝对精度, 别用 == 0。
时间复杂度O(N log(precision)) — 每轮判定 O(N)
空间复杂度O(N) — 前缀和数组(或 O(1) 用单调队列)
注意 · 局限答案非整数时需二分到指定精度(通常 1e-5~1e-7); 若数组可含大量负值, 判定依然正确(子段和可能为负); 要求输出子段端点时需在判定中记录; 单调队列优化仅当窗口上界也有约束时更必要(仅下界可用前缀和+min 扫描)。

易错点

例题

OJ题号难度考点链接
LuoguP14193寻找段落: 长度在[L,R]的最大平均值, 二分+单调队列, 经典分数规划模板https://www.luogu.com.cn/problem/P1419
LuoguP23963均分纸牌变体: 二分答案+前缀和判定场景https://www.luogu.com.cn/problem/P2396
POJ20183Best Cow Fences: 长度>=F的最大平均子段, 二分+判定经典http://poj.org/problem?id=2018
步骤迷你图(节点 = 算法步骤 · 边 = 流程 · 绿色 = 起始步骤)
上游 · 本知识点依赖(前置/变体/组合/共现)

信源

✓ = 已核验 · △ = 未核验