给你一个下标从 0 开始的数组 nums
,它含有 n
个非负整数。
每一步操作中,你需要:
- 选择一个满足
1 <= i < n
的整数i
,且nums[i] > 0
。 - 将
nums[i]
减 1 。 - 将
nums[i - 1]
加 1 。
你可以对数组执行 任意 次上述操作,请你返回可以得到的 nums
数组中 最大值 最小 为多少。
示例 1:
输入:nums = [3,7,1,6] 输出:5 解释: 一串最优操作是: 1. 选择 i = 1 ,nums 变为 [4,6,1,6] 。 2. 选择 i = 3 ,nums 变为 [4,6,2,5] 。 3. 选择 i = 1 ,nums 变为 [5,5,2,5] 。 nums 中最大值为 5 。无法得到比 5 更小的最大值。 所以我们返回 5 。
示例 2:
输入:nums = [10,1] 输出:10 解释: 最优解是不改动 nums ,10 是最大值,所以返回 10 。
2439. 最小化数组中的最大值
说实话,没这个例子,我都不知道题干在说什么,😂😂😂
第一个眼看,没什么思路,那就先用题目的要求进行模拟。
按照题目要求,2个相邻的数字经过若干次计算之后,肯定满足 num[i-1] >= num[i],
- 如果2个数字的和为偶数, num[i-1] = num[i] = 平均数
- 如果2个数字的和是奇数的话,num[i-1] 肯定是较大的那个
- 比如2个数字为1和8,和为9,平均数为(1 + 8) / 2 = 4,剩下的较大的数字为5,将平均数赋值给num[i],剩下较大的数组赋值给num[i-1],
搞定了一次循环之后,不一定完成了最大值的调整,上面的一轮遍历,只是保证了相近的2个数字满足了条件,不能保证经过调整后所有的数字都满足条件。
- 比如[1,5,10], 经过上面的一轮模拟,变成了[3,3,10] -> [3,7,6],7还有调整的空间
- 在经过一轮遍历,由[3,7,6] -> [5,5,6], -> [5,6,5],
- 此时虽然肉眼已经知道了结果,但是不满足所有的数字都满足num[i-1] >= num[i],在经过一轮遍历,变成 [6,5,5], 此时才能让所有的数字都满足num[i-1] >= num[i],才是外层循环的真正结束条件。
经过上面的模拟,目前知道需要经过2层循环,内部循环是保证相邻的2个数字满足num[i-1] >= num[i],外层循环保证所有的数字都满足num[i-1] >= num[i]。
在写的过程中,感觉算法非常像冒泡排序算法,冒泡只是单纯的交换相邻值,而此算法在交换相邻值的同时,计算了平均值并进行赋值。
算法需要内外2层循环,O(n^2)的时间复杂度,放上去果然出现超时,不过计算结果是没问题的。
class Solution {
func minimizeArrayValue(_ nums: [Int]) -> Int {
var tempNums = nums
var maxValue = 0
var hasChange = false
// 按照题目要求模拟,后一个数字大于前一个数字,就计算2者的平均值,较大值给i-1,较小值给i
// 最差情况是对连续递增的数组,需要循环O(n^2),有点冒泡算法的感觉,每次循环都把较大值往前冒泡一次,执行到最后,最大值就在数组的第一个
// 比如[1,2,3,4],按照此算法进行模拟,
// 第一次循环, [2,1,3,4],[2,2,2,4],[2,2,3,3]
// 第二次循环, [2,3,2,3],[2,3,3,2]
// 第三次循环, [3,2,3,2],[3,3,2,2]
repeat {
hasChange = false
for (i,value) in tempNums.enumerated() {
if i>0 {
let preValue = tempNums[i-1]
if value > preValue {
// midValue 相当于是向下取整了, 比如 (1 + 8) / 2 = 4, 另一个较大的数组通过减法算出
// 较小值给到i, 较大值给到i-1, 一步一步把最大值冒泡到数组第一个位置
let midValue = (value + preValue)/2
tempNums[i-1] = value + preValue - midValue
tempNums[i] = midValue
hasChange = true
}
}
}
} while hasChange == true
maxValue = tempNums.first!
return maxValue
}
}
既然我们没有思路,那就看看其他人的。
进阶思路1,二分查找:
给你一个数组,对于1 <= i < len(nums)
的i
可以有以下操作
- 将
nums[i]--
,nums[i-1]++
nums[i] > 0
对于这个条件,我们应该得到这样的理解:
- 前方的较小数可以接受后方较大数多余的数字
可能这句话有些晦涩难懂,下面举一个例子具体分析
设nums = [2,3,7,1,6]
由对前三个数进行操作,则我们可以得到的最小最大值为4
怎么做到的捏?我们来一步步走
[2,3,7]
[3,2,7]
[4,1,7]
[4,2,6]
[4,3,5]
[4,4,4]
一步步下来,我们发现,前方的较小的2和3承接了来自后方的7中的数,最终使得整个数组都整体变小了
2承载了最终答案4中的,来自于7中的两个1
3承载了最终答案4中的,来自于7中的一个1
由此我们可以由局部推广到整体,我们只需要检查数组在小数承载大数的基础上,是否可以全部都不大于k.
那么要检查的数从哪里来?
以下2种都可以
- 思路1,按照题目要求,从0到 10^9,开始二分查找。
- 思路2,遍历一次数组,从数组的最小值到数组中的最大值开始二分。
class Solution {
/// 检查数字k能否满足承载需求
/// - Parameters:
/// - nums: 原始数组
/// - k: 本次检查的k值
/// - Returns:
/// true: 数组遍历完成,并且数组内的值可以承载,表示k是最大值中的一个,但不一定是最小的最大值;
/// false: 数组内已经发现比k更大的值,后续遍历无意义
func check(_ nums: [Int], k: Int) -> Bool {
//前方的数字还可以帮我们后方的大数承载多少数字
var extra = 0
for value in nums {
if (value <= k) { //当前值小于目标值,可以接受 k-value的承载
extra += k - value
} else {
//当前值大于目标值,承载量 减去对应的差值 value-k
extra -= value - k
if extra < 0 { // extra < 0,表示到第i个位置的最大值已经>k,后续已经无需再比对了
return false
}
}
}
return true
}
func minimizeArrayValue(_ nums: [Int]) -> Int {
var left = 0
var right = 10 * 10000 * 10000
// 二分答案,二分范围从0-10^9中寻找答案,
// mid是本次查找预设的一个答案,检验这个答案是否满足要求
// 如果满足check条件,说明可以继续向下查找,缩小右边界
// 如果不满足check条件,把左边界加1,
// 最终左右边界相等时,一定是左边界-1是不满足check条件,左边界及右侧都满足check条件
while left < right {
let mid = left + (right - left) / 2
let canExtra = check(nums, k: mid)
if canExtra {
right = mid
} else {
left = mid + 1
}
}
return left
}
}
时间复杂度是O(N*logN),空间复杂度为O(1),
进阶思路2,分类讨论:
削峰填谷,整体考虑,遇到第i个时,把第i个当成最后一个,把前i-1个都当成一个数字进行处理。计算前i个数字的平均值向上取整,此时相当于把前i个的山峰削减完成,前i个的最大值就是平均值向上取整与之前最大值的比较结果,然后继续第i+1个。
从 nums[0] 开始讨论:
- 如果数组中有 nums[0],那么最大值为 nums[0]。
- 再考虑 nums[1],
- 如果 nums[0]>=nums[1],num[1]是山谷,最大值还是 nums[0],不用处理
- 如果 nums[0]<nums[1],说明num[1]是一个山峰,则应该平均这两个数,平均后的最大值向上取整,即(nums[0]+num[1])/2向上取整,与之前的山峰进行比对更新
- 再考虑 nums[2],
- 如果前面算出的最大值 >= nums[2] ,num[2]就是一个山谷,最大值不变,不用处理;
- 如果前面算出的最大值 < nums[2] ,说明num[1]是一个山峰,那么需要平均这三个数向上取整,与之前的山峰进行对比更新。
- .....
- 对于任意一个num[i],
- 如果前面算出的最大值 >= nums[i] ,num[i]就是一个山谷,最大值不变,不用处理;
- 如果前面算出的最大值 < nums[i] ,说明num[i]是一个山峰,那么需要平均前i个数字向上取整,与之前的山峰进行对比更新。
以此类推直到最后一个数。
过程中的最大值为答案。
为什么要向上取整?
因为数组内的总和是不变的,并且都为整型,直接使用平均值计算出来的为浮点型,向上取整计算出来的才是平均后的最大值。
为什么计算出前i个数字平均值向上取整后还需要和之前的山峰进行对比?
因为前一个山峰可能是第3个值,后续的第4-10都是山谷,到第11时才又遇到山峰,此时计算出的前11个平均值可能小于第3个值。
怎么向上取整?
正常思路,计算好平均数,使用ceil函数取整
let avg = Double(sum)/Double(i+1)
result = max(result, Int(ceil(avg)))
牛B思路,先对sum+i,在计算除法,计算结果必定是向上取整的。
(sum+i)/(i+1)
为什么是除以i+1?
因为下表是i,从0开始计数,共有i+1个数字
class Solution {
func minimizeArrayValue(_ nums: [Int]) -> Int {
var sum = 0
var result = nums.first!
for (i,value) in nums.enumerated() {
sum += value
if result < value {
// 正常取整
// let avg = Double(sum)/Double(i+1)
// result = max(result, Int(ceil(avg)))
// 牛B取整
result = max(result, (sum+i)/(i+1))
}
}
return result
}
}
果然好的思路代码也简洁,只需要一次遍历, 时间复杂度为O(n), 空间复杂度为O(1).
总结来看, 分类讨论的这种算法效率最高,代码最简洁。
路漫漫其修远兮,吾将上下求索。