二分法的例题 —— 410. 分割数组的最大值(Kotlin)
题目描述
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意:
数组长度 n 满足以下条件:
1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)
示例:
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
解决方案
class Solution {
fun splitArray(nums: IntArray, m: Int): Int {
// 由于是非负整数组,所以最小值为1,用Long保存的原因是防止数字越界
var left = 1L
// 最大值为总和+1 的作用是保证结果在left和right范围内,否则left可能会一直扩大,导致ans无法赋值
var right = 1L
nums.forEach {
right += it
}
// 二分求解
var ans = 0L
while (left < right) {
val middle = (left + right) / 2
if (canSplit(nums, m, middle)) {
// 可行,记录答案,缩小数组总和
ans = middle
right = middle
} else {
// 不可行,尝试更大的数组总和
left = middle + 1
}
}
return ans.toInt()
}
/**
* 判断nums能否分成不大于maxCount组,且每组总和不大于maxSum的数组
*/
private fun canSplit(nums: IntArray, maxCount: Int, maxSum: Long): Boolean {
// 记录当前分组的总和
var sum = 0L
// 记录当前已分组的数量
var count = 0
// 尝试分组
for (num in nums) {
// 如果单个数字大于maxSum,无法分割出和小于maxSum的一组
if (num > maxSum) return false
// 如果和超过了maxSum,另起一组
if (sum + num > maxSum) {
count++
sum = num.toLong()
} else {
// 贪心策略,一组中放的数字尽可能多
sum += num
}
}
// count < maxCount 而不是 count <= maxCount 的原因是:分割完成后,sum中的数字是一组,没计入count中
return count < maxCount
}
}
解题思路
1.本题的单调关系关系比较隐蔽,单调关系是:数组和的最大值越小,分组数越大。数组和的范围是可以确定的。
2.根据单调关系,将题目转换为:当子数组的和最大为maxSum时,至少需要分多少组,能否在最多m组的限制范围内完成分割。
3.由于是非空非负整数数组,最小值可设为1;数组和的最大值设置为nums的总和+1,+1的作用是保证结果在最小值和最大值范围内。采用Long保存中间值的作用是防止数组越界。
4.canSplit中用到了贪心策略,在子数组中尽可能多的放置元素,直到放不下,另起一组。
5.如果当前值满足分割条件,记录当前值,利用二分法,缩小子数组总和。否则扩大子数组总和,直到找到最佳答案。
推荐阅读:
我的LeetCode题解,使用kotlin语言: