ESC
输入关键词搜索文章
目录

分割数组的最大值

LeetCode · 410 · Hard
二分答案的模板题:最小化最大段和
Hard难度
59%通过率
LeetCode来源
二分算法标签
Part 0
题目描述

给定一个非负整数数组 $nums$ 和一个整数 $k$,将数组分成 $k$非空连续子数组,使得这 $k$ 个子数组各自和的最大值最小。返回分割后最小的和的最大值。

形式化定义

将数组 $nums[0..n{-}1]$ 切分为 $k$ 段连续子数组 $[0..p_1], [p_1{+}1..p_2], \ldots, [p_{k-2}{+}1..n{-}1]$。设第 $j$ 段的段和为 $S_j$,目标是:

$$\min \max_{1 \le j \le k} S_j$$
问题本质:在所有合法的 $k$ 段连续划分方案中,找到使"最大段和"最小的那个方案。这是一个经典的 minimax 优化问题。

Part 1
样例分析

样例 1

输入nums = [7,2,5,10,8], k = 2

输出18

解释:最优划分 $[7,2,5] \mid [10,8]$,段和分别为 $14$$18$,最大值为 $18$。其他方案的最大值都更大:

  • $[7] \mid [2,5,10,8]$$\max(7, 25) = 25$
  • $[7,2] \mid [5,10,8]$$\max(9, 23) = 23$
  • $[7,2,5,10] \mid [8]$$\max(24, 8) = 24$

样例 2

输入nums = [1,2,3,4,5], k = 2

输出9

解释:最优划分 $[1,2,3,4] \mid [5]$,最大段和为 $9$

Part 2
核心思路

直接枚举所有划分方案的组合数是指数级的。但注意到一个关键性质:

关键观察:单调性

观察 1:如果"最大段和不超过 $T$"有合法划分方案,那么对于任意 $T' > T$,也一定有合法方案(只需沿用同一个划分)。

观察 2:答案关于 $T$ 具有单调性——存在一个临界值 $T^*$$T \ge T^*$ 时都有解,$T < T^*$ 时都无解。这正是二分答案的适用条件。

观察 3:给定 $T$,用贪心就能判断可行性——从左到右尽量往当前段塞元素,塞不下就断开新段。最终段数 $\le k$$T$ 可行。

二分答案的搜索范围:

  • 下界 $L = \max(nums)$:最大段和至少要能装下最大的单个元素。
  • 上界 $R = \sum(nums)$:所有元素放一段。

通用模式

「最小化最大值」或「最大化最小值」类问题,如果可行性关于答案具有单调性,几乎都可以用二分答案。这类题目在笔试中出现频率极高,流水线并行题 P4982 就是同一模式的加强版(多了显存约束)。

Part 3
贪心验证的详细逻辑

给定阈值 $T$,判断"最大段和 $\le T$ 时,能否划分为 $\le k$ 段":

维护当前段和 $t$(初始 $0$)和段间断裂次数 $cnt$(初始 $0$)。遍历每个元素 $num$

$$\text{如果 } t + num \le T \text{,加入当前段:}\; t \leftarrow t + num$$
$$\text{否则,断开新段:}\; cnt \leftarrow cnt + 1,\; t = num$$

处理完所有元素后,$cnt$ 是断裂次数 = 段数 $- 1$。判断条件:

$$cnt < k \iff \text{段数} \le k$$

为什么贪心是对的?在固定 $T$ 的前提下,每段"尽量多塞"不会比"提前断开"更差——提前断开只会增加段数。贪心地让每段尽可能长,得到的段数最少,是最有希望满足 $\le k$ 的策略。

二分模板:使用 $L = \max(nums) - 1$$R = \sum(nums) + 1$ 的开区间写法。循环条件 $L + 1 \neq R$,每次 $mid = \lfloor(L+R)/2\rfloor$。若 check 可行则 $R = mid$,否则 $L = mid$。最终 $R$ 即为答案。这样 $L$ 始终停留在"不可行"侧,$R$ 始终停留在"可行"侧,循环结束时 $R$ 就是第一个可行值。
Part 4
代码实现

算法步骤:

  1. 二分范围 $L = \max(nums) - 1$$R = \sum(nums) + 1$
  2. 对每个 $mid$,贪心扫描统计断裂次数 $cnt$
  3. $cnt < k$(可行),收缩右界;否则收缩左界。
  4. 返回 $R$

class Solution:
    def splitArray(self, nums: List[int], k: int) -> int:

        def check(T):
            cnt = 0   # 断裂次数 = 段数 - 1
            t = 0     # 当前段和
            for num in nums:
                if t + num <= T:
                    t += num
                else:
                    cnt += 1
                    t = num
            return cnt < k   # 断裂次数 < k 等价于段数 ≤ k

        L = max(nums) - 1    # L 停在"不可行"侧
        R = sum(nums) + 1    # R 停在"可行"侧

        while L + 1 != R:
            mid = (L + R) // 2
            if check(mid):
                R = mid      # 可行,尝试更小
            else:
                L = mid      # 不可行,需要更大

        return R
  
边界处理$L$ 初始设为 $\max(nums) - 1$(比最小可行答案小 1),确保 $L$ 侧始终不可行。$R$ 初始设为 $\sum(nums) + 1$(所有元素放一段一定可行),确保 $R$ 侧始终可行。循环结束时 $R$ 恰好是答案。
Part 5
复杂度分析
维度复杂度说明
时间$O(n \log(\sum nums))$二分 $\log(\sum nums)$ 轮,每轮 $O(n)$ 贪心扫描
空间$O(1)$只用到常数额外变量

对于 $n \le 1000$$\sum nums \le 10^9$ 的数据规模,$\log_2(10^9) \approx 30$,总操作量约 $3 \times 10^4$,效率极高。

相关题目
参考来源