Skip to content

1760.袋子里最少数目的球

binary search, https://leetcode.cn/problems/minimum-limit-of-balls-in-a-bag/

给你一个整数数组 nums ,其中 nums[i] 表示第 i 个袋子里球的数目。同时给你一个整数 maxOperations

你可以进行如下操作至多 maxOperations 次:

  • 选择任意一个袋子,并将袋子里的球分到 2 个新的袋子中,每个袋子里都有 正整数 个球。
    • 比方说,一个袋子里有 5 个球,你可以把它们分到两个新袋子里,分别有 1 个和 4 个球,或者分别有 2 个和 3 个球。

你的开销是单个袋子里球数目的 最大值 ,你想要 最小化 开销。

请你返回进行上述操作后的最小开销。

示例 1:

输入:nums = [9], maxOperations = 2
输出:3
解释:
- 将装有 9 个球的袋子分成装有 6 个和 3 个球的袋子。[9] -> [6,3] 。
- 将装有 6 个球的袋子分成装有 3 个和 3 个球的袋子。[6,3] -> [3,3,3] 。
装有最多球的袋子里装有 3 个球,所以开销为 3 并返回 3 。

示例 2:

输入:nums = [2,4,8,2], maxOperations = 4
输出:2
解释:
- 将装有 8 个球的袋子分成装有 4 个和 4 个球的袋子。[2,4,8,2] -> [2,4,4,4,2] 。
- 将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,4,4,4,2] -> [2,2,2,4,4,2] 。
- 将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,2,2,4,4,2] -> [2,2,2,2,2,4,2] 。
- 将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,2,2,2,2,4,2] -> [2,2,2,2,2,2,2,2] 。
装有最多球的袋子里装有 2 个球,所以开销为 2 并返回 2 。

示例 3:

输入:nums = [7,17], maxOperations = 2
输出:7

提示:

  • 1 <= nums.length <= 10^5
  • 1 <= maxOperations, nums[i] <= 10^9
python
class Solution:
    def minimumSize(self, nums: List[int], maxOperations: int) -> int:
        def can_achieve(threshold):
            operations = 0
            for num in nums:
                if num > threshold:
                    # 计算将num分成不超过threshold需要的操作次数
                    operations += (num - 1) // threshold
            return operations <= maxOperations

        left, right = 1, max(nums) + 1
        ans = 0
        while left < right:
            mid = (left + right) // 2
            if can_achieve(mid):
                ans = mid
                right = mid
            else:
                left = mid + 1
        return ans

思路:一开始想的不是二分 思路是正着平均拆分最大数 题解的二分法有一点逆向思维的意思 先选定一个数为最大值的最小值 再用maxOperations来判断指针变化。

python
from typing import List


class Solution:
    def minimumSize(self, nums: List[int], maxOperations: int) -> int:
        left, right, ans = 1, max(nums) + 1, 0
        while left < right:
            y = (left + right) // 2
            ops = sum((x - 1) // y for x in nums)
            if ops <= maxOperations:
                ans = y
                right = y
            else:
                left = y + 1
        return ans


if __name__ == "__main__":
    sol = Solution()
    print(sol.minimumSize([9], 2))  # expect 3
    print(sol.minimumSize([2, 4, 8, 2], 4))  # expect 2

为什么 left == right 是结束条件?

我们使用二分查找来寻找最小的最大球数 min_penalty,在 check(mid) 里只是在检查 mid 是否可行,而不是要恰好用掉 maxOperations

  • operations == maxOperations 并不意味着是最优解,因为可能存在更小的 mid 也满足 operations <= maxOperations
  • left == right 表示我们已经收敛到最小的可行 mid,即满足 operations <= maxOperations 且不能再更小。
python
from typing import List

class Solution:
    def minimumSize(self, nums: List[int], maxOperations: int) -> int:
        # 边界情况处理
        if len(nums) == 1:
            return (nums[0] + maxOperations) // (maxOperations + 1)

        def check(n):
            """检查是否可以通过不超过maxOperations次操作将所有数分割为不大于n的块"""
            operations_needed = 0
            for num in nums:
                # 计算需要的操作次数以确保每个数字被分割成最多为n的部分
                operations_needed += (num - 1) // n
                if operations_needed > maxOperations:
                    return False
            return True
        
        # 初始化二分查找的边界
        left, right = 1, max(nums) 
        while left < right:
            mid = (left + right) // 2
            if check(mid):
                right = mid
            else:
                left = mid + 1
        
        return left

题意解读

「最小化最大值」说人话就是,尽量均匀地分配小球。

思路

假设最终每个袋子的球数都至多为 m,那么 m 越小,操作次数就越多,m 越大,操作次数就越少,有单调性,可以二分答案。或者说,看到「最小化最大值」就要先思考二分。

现在问题变成:

根据您提供的图片文字信息,以下是提取的内容:

给定 m ,要求最终每个袋子的球数都至多为 m ,能否在 $ \text{maxOperations} $次操作内完成?

对于 $x = \text{nums}[i] $,假设分成 k 个袋子,每个袋子都至多装 m 个球。 k 不能太小,否则没法一共装 x 个球,所以 km 至少要是 x ,即

$ km \geq x $

解得

$ k \geq \left\lceil \frac{x}{m} \right\rceil $

所以对于 x ,操作次数为

$ \left\lceil \frac{x}{m} \right\rceil - 1 $

减一是因为操作 1 次分出 2 个袋子,操作 2 次分出 3 个袋子……依此类推,操作 k-1 次分出 k 个袋子。

累加操作次数,判断总操作次数与 $ \text{maxOperations} $ 的大小关系。

细节

下面代码采用闭区间。

  • 左端点初始值:1。每个袋子的球数至少是 1。
  • 右端点初始值:$ \max(\text{nums}) $。

关于上取整的计算,当 ( a ) 和 ( b ) 均为正整数时,我们有

$ \left\lceil \frac{a}{b} \right\rceil = \left\lfloor \frac{a-1}{b} \right\rfloor + 1 $

讨论 a 被 b 整除,和不被 b 整除两种情况,可以证明上式的正确性。

所以有

$ \left\lceil \frac{x}{m} \right\rceil - 1 = \left\lfloor \frac{x-1}{m} \right\rfloor $

作者:灵茶山艾府 链接:https://leetcode.cn/problems/minimum-limit-of-balls-in-a-bag/solutions/3071967/er-fen-da-an-pythonjavaccgojsrust-by-end-g7l7/