目录

题目描述

410. 分割数组的最大值

题意分析

输入是一个非负整数数组和一个正整数 k,要求把数组切成恰好 k 段。「段」的含义被限定得很死:每一段都是原数组里一段连续的元素,不能跳着挑,也不能重排顺序,并且每段都不能为空。

要输出的不是某种切法,而是一个数值:在所有合法切法里,把「各段和的最大值」这个指标取到最小时,那个最小值是多少。换句话说,评价一种切法好坏的标准,只看它最重的那一段有多重;我们要找的是「最重的一段」尽可能轻的方案。

约束里有两条信号值得注意。第一,元素都是非负数,这意味着往一段里继续塞元素,段和只会增不会减,「装不下就换一段」这种朴素判断才是可靠的。第二,数据规模上数组长度上千、元素值上百万,总和量级在 $10^9$ 以内,用 int 存总和不会溢出,但按「切法」枚举显然是天文数字。

边界情况也要想清楚。k 等于 1 时只有一段,答案就是总和;k 等于数组长度时每个元素独占一段,答案就是数组最大值。这两个极端顺带说明了答案的取值范围:不可能小于数组最大值(那个元素总要落在某一段里),也不可能大于总和。

解法:二分答案 + 贪心判定

核心思路

直接枚举切点或使用区间 DP 都较重。更适合面试的做法是二分「最大子数组和」这个答案。

答案下界是数组最大值 max(nums),因为任何元素都必须属于某一段;上界是数组总和 sum(nums),因为把全部元素放进一段一定可行。

对候选上限 limit,从左到右贪心分段:当前元素还能放进本段就继续放,超过 limit 才新开一段。这个策略让每一段都尽量向右延伸。任意合法方案的第一刀都不可能比贪心更靠右;去掉第一段后同理,因此贪心得到的是该上限下的最少段数

可行性定义为「最少段数不超过 k」。由于元素非负,如果能分成少于 k 段,还可以继续拆分非空段,直到恰好 k 段,最大段和不会增加。于是它与题目的“恰好 k 段”等价。

limit 越大,需要的段数只会更少,因此可行性呈现“前面不可行、后面可行”的单调结构。二分找到第一个可行的 limit,就是最小化后的答案。循环始终保证答案位于闭区间 [left, right] 中。

解题步骤

  1. 扫描数组,令 left = max(nums)right = sum(nums)
  2. left < right 时,取 mid = left + (right - left) / 2
  3. 用贪心检查 mid:累加当前段,若加入 num 后超过 mid,就以 num 开启新段。
  4. 若段数不超过 kmid 可行,令 right = mid;否则令 left = mid + 1
  5. 当区间收敛时返回 left

[7, 2, 5, 10, 8]k = 2,初始区间是 [10, 32]。候选值依次为 21(可行)、15(不可行)、18(可行)、17(不可行),最终收敛到 18,对应 [7,2,5][10,8]

代码实现

class Solution {
    public int splitArray(int[] nums, int k) {
        int left = 0;
        int right = 0;
        for (int num : nums) {
            left = Math.max(left, num);
            right += num;
        }

        while (left < right) {
            int mid = left + (right - left) / 2;
            if (canSplit(nums, k, mid)) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    private boolean canSplit(int[] nums, int k, int limit) {
        int parts = 1;
        int sum = 0;
        for (int num : nums) {
            if (sum + num > limit) {
                parts++;
                sum = num;
                if (parts > k) {
                    return false;
                }
            } else {
                sum += num;
            }
        }
        return true;
    }
}
func splitArray(nums []int, k int) int {
    left, right := 0, 0
    for _, num := range nums {
        if num > left {
            left = num
        }
        right += num
    }

    for left < right {
        mid := left + (right-left)/2
        if canSplit(nums, k, mid) {
            right = mid
        } else {
            left = mid + 1
        }
    }
    return left
}

func canSplit(nums []int, k, limit int) bool {
    parts, sum := 1, 0
    for _, num := range nums {
        if sum+num > limit {
            parts++
            sum = num
            if parts > k {
                return false
            }
        } else {
            sum += num
        }
    }
    return true
}

复杂度分析

设数组长度为 n、总和为 S、最大值为 M

  • 时间复杂度:$O(n(1 + \log(S-M+1)))$。初始扫描是 $O(n)$,之后每个二分候选值都要用 $O(n)$ 做一次贪心判定。
  • 空间复杂度:$O(1)$。

关键点总结

  • 「最大值最小化」常可转成判定问题,再对具有单调性的答案域二分。
  • 搜索边界不是经验值:最大元素是必要下界,总和是必然可行的上界。
  • 贪心必须给出最少段数;其依据是每一刀都推迟到不能再放的位置。
  • 判定使用“不超过 k 段”,非负数组下可以继续拆成恰好 k 段。

易错点总结

  • 在数组下标上二分:本题单调的是候选段和,不是切点位置。
  • 可行时令 right = mid - 1mid 可能正是第一个可行值,不能排除。
  • 不可行时令 left = mid:向下取整时区间可能不再缩小,导致死循环。
  • 新开一段时忘记令 sum = num:当前元素会丢失,段数被低估。
  • 要求贪心结果必须等于 k:少于 k 段仍然可行,可以继续拆分。
  • 排序后再分段:会破坏子数组必须连续且保持原顺序的约束。

相似题目

题目 难度 考察点
69. x 的平方根 简单 判定只需一次乘法,不涉及数组扫描
644. 子数组最大平均数 II 困难 二分实数而非整数,判定要靠前缀和找非负区间
668. 乘法表中第k小的数 困难 二分的是「第 k 小」,判定按行统计个数而非分段
719. 找出第 K 小的数对距离 困难 判定用双指针统计数对,需先排序
774. 最小化去加油站的最大距离 困难 实数二分且判定是「新增多少个点」而非「切成多少段」
875. 爱吃香蕉的珂珂 中等 分组由堆数固定,判定是向上取整求和
878. 第 N 个神奇数字 困难 判定用容斥与最小公倍数计数,还要取模
1011. 在 D 天内送达包裹的能力 中等 与本题几乎同构,只是段和换成载重、段数换成天数
1201. 丑数 III 中等 判定是三个数的容斥计数,需要防乘法溢出
1231. 分享巧克力 困难 求「最小段和最大」,贪心方向与本题相反
1482. 制作 m 束花所需的最少天数 中等 二分的是天数,判定要数连续可用段的长度
1552. 两球之间的磁力 中等 最小间距最大化,判定是贪心放球计数
LCP 12. 小张刷题计划 中等 分段时允许免费跳过段内最大值,贪心多一层决策
LCR 072. x 的平方根 简单 单调函数求整数根,需处理平方溢出
LCR 073. 爱吃香蕉的狒狒 中等 上界取最大堆而非总和,判定按堆独立计算
补充题 7. 木头切割问题 中等 判定用整除累加根数,段长可自由取而非受连续限制
补充题 20. 立方根 中等 实数二分,收敛条件靠精度阈值而非区间相等