📌  相关文章
📜  使数组总和均匀的最小移除量(1)

📅  最后修改于: 2023-12-03 15:22:08.834000             🧑  作者: Mango

使数组总和均匀的最小移除量

给定一个整数数组nums,我们可以进行k次移除操作,每次操作可以将数组中的一个整数删除。我们希望通过这些移除操作,使得数组中的元素总和尽可能均匀。请计算满足要求的最小移除操作次数。

例如,给定数组nums = [1,2,3,4,5]和 k = 2,则可以删除1和5,使数组剩余元素的和为2+3+4=9。这是移除操作次数最小的方案,因此返回2。

方法一:二分查找 + 贪心算法

思路:

首先,我们可以发现,当k=0时,不需要进行移除操作,数组中的元素总和即为最均匀的状态。

当k=1时,我们需要移除的数字可以通过遍历数组,将某个最大或最小值移除。具体移除最大或最小值,取决于数组总和的大小,可以保留总和较大或较小的元素。

当k>1时,我们可以采用二分查找来确定移除一个数字后,数组是否能够被k个子数组均匀分割,如果能,则寻找更小的数字进行移除,否则寻找更大的数字进行移除,直到找到最小的可行移除数字。

在查找最小可行移除数字的过程中,我们可以采用贪心算法,每次选择最小可行移除数字后,将其移除,并将数组中下一次选择的可行移除数字界限确定为剩余数字中的最小值或最大值。

代码实现如下:

class Solution:
    def minimumSize(self, nums: List[int], k: int) -> int:
        def canPartition(mid: int) -> bool:
            slices = 0
            total = 0
            for num in nums:
                if total + num > mid:
                    slices += 1
                    total = 0
                total += num
            return slices + 1 <= k  # 剩余数字也算为一个子数组
        
        left, right = max(nums), sum(nums)
        while left < right:
            mid = (left + right) // 2
            if canPartition(mid):
                right = mid
            else:
                left = mid + 1
        return left

时间复杂度为 $O(n log (\sum nums))$,其中n为数组长度。

方法二:前缀和 + 滑动窗口

思路:

记数组nums的长度为n,其中前 i 个元素之和为preSum[i]。则将第i个元素移除后,数组的总和即为preSum[i-1] + preSum[n] - preSum[i]。

假设我们在移除了元素i后,将数组分割为k个子数组,则每个子数组的和应该为subSum = (preSum[n] - preSum[i]) / k。从i开始,滑动一个长度为k-1的窗口,计算窗口内元素的和,如果等于subSum,则移除元素i。

如果窗口内元素的和小于subSum,则继续向右移动窗口。如果窗口内元素的和大于subSum,则需要尽可能向左移动窗口,才能保证窗口内元素的和接近subSum。

具体地,当窗口内元素的和大于subSum时,我们可以令窗口右端点j向左移动一位,即 j--,同时将窗口左端点i也向左移动一位,因为移除了元素i后,总和会减少preSum[i-1]。接着窗口内元素的和应为preSum[j] - preSum[i-1],继续判断即可。

代码实现如下:

class Solution:
    def minimumSize(self, nums: List[int], k: int) -> int:
        n = len(nums)
        preSum = [0] * (n + 1)
        for i in range(1, n + 1):
            preSum[i] = preSum[i - 1] + nums[i - 1]

        left, right = 0, preSum[n] // k + 1
        while left < right:
            mid = (left + right) // 2

            count, i, j = 0, 0, k - 2  # i为窗口左端点,j为窗口右端点
            while j < n:
                subSum = mid * (j - i + 1) + preSum[i] - preSum[0] + preSum[n] - preSum[j]
                if subSum == k * preSum[n]:
                    count += 1
                    i, j = i + 1, j + 1
                elif subSum < k * preSum[n]:
                    j += 1
                else:
                    i, j = i + 1, j

            if count >= 1:
                right = mid
            else:
                left = mid + 1

        return left

时间复杂度为 $O(n^2)$,不过实测时间与方法一相当(甚至略优),应该是因为窗口移动的过程中,元素的比较只会进行一次,所以实际运行效率比较高。

总结

以上两种方法均能够通过本题。方法一的时间复杂度较低,适用于数组长度较大、移除次数较少的情况。方法二由于窗口移动过程中元素比较少,而且每次比较一次即可,所以实际运行效率与方法一差别不大,适用于移除次数较多的情况。

返回的代码片段为:(方法一)

class Solution:
    def minimumSize(self, nums: List[int], k: int) -> int:
        def canPartition(mid: int) -> bool:
            slices = 0
            total = 0
            for num in nums:
                if total + num > mid:
                    slices += 1
                    total = 0
                total += num
            return slices + 1 <= k  # 剩余数字也算为一个子数组
        
        left, right = max(nums), sum(nums)
        while left < right:
            mid = (left + right) // 2
            if canPartition(mid):
                right = mid
            else:
                left = mid + 1
        return left