leetcode 2439. 最小化数组中的最大值

Posted 想名真难

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了leetcode 2439. 最小化数组中的最大值相关的知识,希望对你有一定的参考价值。

给你一个下标从 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).

总结来看, 分类讨论的这种算法效率最高,代码最简洁。

路漫漫其修远兮,吾将上下求索。

以上是关于leetcode 2439. 最小化数组中的最大值的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode1856.子数组最小乘积的最大值 Maximum Subarray Min-Product(Java)

LeetCode 2091. 从数组中移除最大值和最小值

Leetcode刷题100天(阿里云周赛)—最大数和最小数—day42

Leetcode刷题100天(阿里云周赛)—最大数和最小数—day42

Leetcode——数组中最大数对和的最小值

LeetCode 1877 数组中最大数对和的最小值[排序] HERODING的LeetCode之路