「一本通 1.2 练习 1」数列分段 II

GitHub跳转原题关系图返回列表

二分每段和的上限,用 O(n) 贪心判定能否分成不超过 m 段,总复杂度 O(n log Σa)。

OJ: roj

题目 ID: 1436

难度:普及+/提高-

标签:二分答案贪心

创建: 2026-09-30 10:08

更新: 2026-10-07 11:01

形式化题目

给定长度为 nn 的非负整数数列 a1,…,ana_1, \dots, a_n 与整数 mm(m⩽nm \leqslant n),把数列 划分成 mm 个连续的段,最小化所有段和中的最大值。

正解

思路

直接枚举分段方案是指数级的。换一个方向:先猜答案,再验证。

设答案为 xx,即"每段和都不超过 xx"。给定 xx,最少需要几段可以用贪心求出:从左到右 依次累加,当前元素放得进本段就放,放不下就另起一段。这样每一段都尽量装满,得到的段数 f(x)f(x) 就是以 xx 为上限时的最少段数;只要 f(x)⩽mf(x) \leqslant m,就存在一种合法的分法。

关键观察是单调性:xx 越大,每段能装下的元素越多,f(x)f(x) 只会变少或不变。所以 f(x)⩽mf(x) \leqslant m 在 xx 上具有单调性,可以对 xx 二分:

  • 下界取 max⁡ai\max a_i:任何一段至少要装下一个元素;
  • 上界取 ∑ai\sum a_i:整个数列一段分完。

二分中点 midmid,若 f(mid)⩽mf(mid) \leqslant m 则答案 ⩽mid\leqslant mid,收缩右端;否则收缩左端。

用样例「4 2 4 5 1,m=3m=3」演示判定过程,下表展示不同上限 xx 对应的贪心分段与最少段数:

上限 xx 贪心分段 最少段数 f(x)f(x) f(x)⩽3f(x) \leqslant 3?
5 [4] [2] [4] [5] [1] 5 否
6 [4 2] [4] [5 1] 3 是
9 [4 2] [4 5] [1] 3 是
15 [4 2 4 5] [1] 2 是

可以看到 f(x)f(x) 随 xx 增大单调不增,从"否"翻转为"是"的临界点就是答案 66, 与样例输出一致。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-07-05 21:47
# update_at: 2026-07-05 21:47

import sys


def count_segments(a: list[int], limit: int) -> int:
    """上限为 limit 时最少分几段:贪心累加,放不下就另起一段。"""
    segments, cur = 1, 0
    for x in a:
        cur_add = cur + x
        if cur_add <= limit:
            cur = cur_add                        # 还塞得进当前段
        else:
            segments += 1                        # 另起一段
            cur = x
    return segments


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n, m = next(data), next(data)
    a = [next(data) for _ in range(n)]

    # 答案 x 的判定具有单调性:x 越大,最少段数越少,在 [max(a), sum(a)] 上二分
    lo, hi = max(a), sum(a)                      # 每段至少装下最大元素;最多一段装下全部
    while lo < hi:
        mid = (lo + hi) // 2
        can = count_segments(a, mid) <= m        # 上限 mid 时段数不超限 → 答案 ≤ mid
        if can:
            hi = mid
        else:
            lo = mid + 1

    print(lo)


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-05 01:00
 * update_at: 2026-10-05 01:00
 */
#include <cstdio>
#include <algorithm>
using namespace std;

typedef long long ll;

const int MAXN = 100005;

int n;       // 数列长度
ll m;        // 要求分成的段数
ll a[MAXN];  // 题目给出的数列,下标从 1 开始

// 判断上限 x 是否可行:贪心从左往右累加,放不下就另起一段,
// 返回以 x 为每段和上限时的最少段数是否不超过 m。
// 贪心正确性:每一段尽量装满,交换论证可证段数不会更多。
bool check(ll x) {
    ll cnt = 1;   // 当前已经用掉的段数,至少有一段
    ll sum = 0;   // 当前段的和
    for (int i = 1; i <= n; ++i) {
        if (sum + a[i] <= x) {
            sum += a[i];      // 还塞得进当前段
        } else {
            cnt++;            // 另起一段
            sum = a[i];
        }
    }
    return cnt <= m;
}

void solve() {
    scanf("%d %lld", &n, &m);
    ll maxa = 0, sumall = 0;   // 二分下界:至少装下最大元素;上界:一段装下全部
    for (int i = 1; i <= n; ++i) {
        scanf("%lld", &a[i]);
        maxa = max(maxa, a[i]);
        sumall += a[i];
    }

    // 答案 x 具有单调性:x 越大,最少段数越少,在 [maxa, sumall] 上二分
    ll lo = maxa, hi = sumall;
    while (lo < hi) {
        ll mid = (lo + hi) / 2;
        if (check(mid)) {
            hi = mid;          // 可行:答案可能是 mid 或更小
        } else {
            lo = mid + 1;      // 不可行:mid 太小
        }
    }

    printf("%lld\n", lo);
}

int main() {
    solve();
    return 0;
}

复杂度

  • 时间:每次判定 O(n)O(n),二分 O(log⁡∑ai)O(\log \sum a_i) 轮,总计 O(nlog⁡∑ai)O(n \log \sum a_i)。 本题 n⩽105n \leqslant 10^5、ai⩽105a_i \leqslant 10^5 量级,非常宽裕。
  • 空间:O(n)O(n) 存数列。

总结

本题是"最大化最小值 / 最小化最大值"类问题的经典范式:**答案本身具有单调性时,二分答案

  • 贪心判定**。贪心判定的正确性来自交换论证——把任何合法方案的第一段右端点右移到贪心 位置仍然合法,逐段调整后段数不会增加。二分结束的条件是左右端点重合,此时 lolo 就是 最小的可行上限。

图示解析

flowchart TD
    A["答案 x"] -->|"判定 f(x) ≤ m ?"| B{"贪心分段"}
    B -->|"是:x 可能更小"| C["右端收缩 hi = mid"]
    B -->|"否:x 太小"| D["左端收缩 lo = mid + 1"]
    C --> E{"lo < hi ?"}
    D --> E
    E -->|"是"| B
    E -->|"否:lo 即答案"| F["输出 lo"]

上图展示二分答案的整体流程:每轮用 O(n)O(n) 贪心求出上限 midmid 下的最少段数,按可行与 否收缩二分区间。观察重点是"判定可行 → 答案在左半"、"判定不可行 → 答案在右半"这一 单调收缩过程,它保证最终左右端点重合处就是最小的可行上限。