410. Split Array Largest Sum (H)
https://leetcode.com/problems/split-array-largest-sum/
Given an array nums which consists of non-negative integers and an integer m, you can split the array into m non-empty continuous subarrays.
Write an algorithm to minimize the largest sum among these m subarrays.
Example 1:
Input: nums = [7,2,5,10,8], m = 2
Output: 18
Explanation:
There are four ways to split nums into two subarrays.
The best way is to split it into [7,2,5] and [10,8],
where the largest sum among the two subarrays is only 18.Example 2:
Input: nums = [1,2,3,4,5], m = 2
Output: 9Example 3:
Input: nums = [1,4,4], m = 3
Output: 4
Constraints:
1 <= nums.length <= 10000 <= nums[i] <= 1061 <= m <= min(50, nums.length)
Solution:
这个题目有点类似前文一道经典动态规划题目 高楼扔鸡蛋,题目比较绕,又是最大值又是最小值的。
简单说,给你输入一个数组 nums 和数字 m,你要把 nums 分割成 m 个子数组。
肯定有不止一种分割方法,每种分割方法都会把 nums 分成 m 个子数组,这 m 个子数组中肯定有一个和最大的子数组对吧。
我们想要找一个分割方法,该方法分割出的最大子数组和是所有方法中最大子数组和最小的。
请你的算法返回这个分割方法对应的最大子数组和。
我滴妈呀,这个题目看了就觉得 Hard,完全没思路,这题怎么能和二分查找算法扯上关系?
说个小插曲,快手面试有一道画师画画的算法题,很难,就是以这道题为原型。当时我没做过这道力扣题,面试有点懵,不过之前文章 二分查找算法运用 写了两道类似的比较简单的题目,外加面试官的提示,把那道题做出来了。
面试做算法题的时候,题目一般都会要求算法的时间复杂度,如果你发现 O(NlogN) 这样存在对数的复杂度,一般都要往二分查找的方向上靠,这也算是个小套路。
言归正传,如何解决这道数组分割的问题?
首先,一个拍脑袋的思路就是用 回溯算法框架 暴力穷举呗,我简单说下思路:
你不是要我把 nums 分割成 m 个子数组,然后计算巴拉巴拉又是最大又是最小的那个最值吗?那我把所有分割方案都穷举出来,那个最值肯定可以算出来对吧?
怎么穷举呢?把 nums 分割成 m 个子数组,相当于在 len(nums) 个元素的序列中切 m - 1 刀,对于每两个元素之间的间隙,我们都有两种「选择」,切一刀,或者不切。
你看,这不就是标准的回溯暴力穷举思路嘛,我们根据穷举结果去计算每种方案的最大子数组和,肯定可以算出答案。
但是回溯的缺点就是复杂度很高,我们刚才说的思路其实就是「组合」嘛,时间复杂度就是组合公式:
时间复杂度其实是非常高的,所以回溯算法不是一个好的思路,还是得上二分查找技巧,反向思考这道题。
现在题目是固定了 m 的值,让我们确定一个最大子数组和;所谓反向思考就是说,我们可以反过来,限制一个最大子数组和 max,来反推最大子数组和为 max 时,至少可以将 nums 分割成几个子数组。
比如说我们可以写这样一个 split 函数:
比如说 nums = [7,2,5,10],若限制 max = 10,则 split 函数返回 3,即 nums 数组最少能分割成三个子数组,分别是 [7,2],[5],[10]。
那如果我们找到一个最小 max 值,满足 split(nums, max) 和 m 相等,那么这个 max 值不就是符合题意的「最小的最大子数组和」吗?
现在就简单了,我们只要对 max 进行穷举就行,那么最大子数组和 max 的取值范围是什么呢?
显然,子数组至少包含一个元素,至多包含整个数组,所以「最大」子数组和的取值范围就是闭区间 [max(nums), sum(nums)],也就是最大元素值到整个数组和之间。
那么,我们就可以写出如下代码:
这段代码有两个关键问题:
1、对 max 变量的穷举是从 lo 到 hi 即从小到大的。
这是因为我们求的是「最大子数组和」的「最小值」,且 split 函数的返回值有单调性,所以从小到大遍历,第一个满足条件的值就是「最小值」。
2、函数返回的条件是 n <= m,而不是 n == m。按照之前的思路,应该 n == m 才对吧?
其实,split 函数采用了贪心的策略,计算的是 max 限制下至少能够将 nums 分割成几个子数组。
举个例子,输入 nums = [2,1,1], m = 3,显然分割方法只有一种,即每个元素都认为是一个子数组,最大子数组和为 2。
但是,我们的算法会在区间 [2,4] 穷举 max,当 max = 2 时,split 会算出 nums 至少可以被分割成 n = 2 个子数组 [2] 和 [1,1]。
当 max = 3 时算出 n = 2,当 max = 4 时算出 n = 1,显然都是小于 m = 3 的。
所以我们不能用 n == m 而必须用 n <= m 来找到答案,因为如果你能把 nums 分割成 2 个子数组([2],[1,1]),那么肯定也可以分割成 3 个子数组([2],[1],[1])。
好了,现在 for 循环的暴力算法已经写完了,但是无法通过力扣的判题系统,会超时。
由于 split 是单调函数,且符合二分查找技巧进行优化的标志,所以可以试图改造成二分查找。
那么应该使用搜索左侧边界的二分查找,还是搜索右侧边界的二分查找呢?这个还是要看我们的算法逻辑:
可能存在多个 max 使得 split(nums, max) 算出相同的 n,因为我们的算法会返回最小的那个 max,所以应该使用搜索左侧边界的二分查找算法。
现在,问题变为:在闭区间 [lo, hi] 中搜索一个最小的 max,使得 split(nums, max) 恰好等于 m。
那么,我们就可以直接套用搜索左侧边界的二分搜索框架改写代码:
这段二分搜索的代码就是标准的搜索左侧边界的代码框架,如果不理解可以参见前文 二分查找框架详解,这里就不展开了。
至此,这道题就通过二分查找技巧高效解决了。假设 nums 元素个数为 N,元素和为 S,则 split 函数的复杂度为 O(N),二分查找的复杂度为 O(logS),所以算法的总时间复杂度为 O(N*logS)。
Last updated
Was this helpful?
