题目重点:非递减数组仍可能有负数;前缀和先降后升,不能直接全区间二分,需从最小值位置查找最右可行长度。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 数组全局非递减,元素可为负、零或正;选取的是从 start 开始连续的前 k 个,而非跳选。
- 比较严格为 sum<target;只返回最大 k,若没有任何非空可行前缀返回 0。
- start 可等于 n,返回 0;不合法 start 或违反排序前提报错。
- 训练版允许 k=0 作为默认无解结果,但不把空前缀称为满足严格不等式。
解题路线
- 从 start 起建局部前缀和 P[k]。因为待加数组非递减,P 的相邻增量先是负数、再可能是零、最后是正数,所以 P 先不增后不减,整体未必单调。
- 用 bisect_left 找第一项非负值的位置,消费所有负数后得到最小前缀和的位置 k0。若 P[k0]≥target,任何非空前缀都不满足。
- 在 k≥k0 的右半段,P 非递减,P[k]<target 构成真前缀;二分最右满足的 k。这个 k 也是全域最长可行长度,因为左半段索引更小。
- 如果只问一次,直接线性扫描全部前缀并更新最大合法 k 更简单也稳;二分在同一数组做大量查询且有前缀和预处理时更有价值。
手动走一遍
[-5,-1,2,4] 从 0 起的非空前缀和依次 -5,-6,-4,0;目标 -3,前三个都严格小于 -3,第四个不满足,答案 3。若草率地假设前缀和一直递增,会在 -5→-6 处违反二分条件。
Python 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
from bisect import bisect_left
from collections.abc import Sequence
def maximum_count_below_target(values: Sequence[int], start: int, target: int) -> int:
"""Maximum count from start with sum strictly below target; 0 if none."""
n = len(values)
if not 0 <= start <= n or any(values[i] > values[i + 1] for i in range(n - 1)):
raise ValueError("invalid start or nondecreasing invariant")
if start == n:
return 0
prefix = [0]
for value in values[start:]:
prefix.append(prefix[-1] + value)
minimum_at = bisect_left(values, 0, lo=start) - start
if prefix[minimum_at] >= target:
return 0
lo, hi = minimum_at, len(prefix)
while lo + 1 < hi:
mid = (lo + hi) // 2
if prefix[mid] < target:
lo = mid
else:
hi = mid
return lo
if __name__ == "__main__":
assert maximum_count_below_target([-5, -1, 2, 4], 0, -3) == 3
复杂度
本代码为单次查询建局部前缀 O(n−start) 时间/空间,并用 O(log n) 找转折与右界;总仍 O(n) 时间。若预先存全局前缀和以支持多查询,每次可用 O(log n) 找转折及右边界,空间 O(n),但应处理不同 start 的平移和负值。
易错点与追问
容易失分的边界
- 把非递减元素误当非负元素,直接对全部前缀和二分。
- 把严格 < 写成 ≤,恰好等于 target 时误算。
- 只从数组开头累加而忽略给定 index。
- 未确认可否返回 0 就称其为原题唯一接口。
继续深挖
- 若输入保证所有元素非负,能否直接在全部局部前缀和上二分?
- 有很多 (start,target) 查询时如何一次预处理全局前缀和?
- 若允许跳选元素而非连续取前缀,问题如何变化?
