1. 题目
给你一个整数数组 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 < = n u m s . l e n g t h < = 1 0 5 1 <= nums.length <= 10^5 1<=nums.length<=105
- 1 < = m a x O p e r a t i o n s , n u m s [ i ] < = 1 0 9 1 <= maxOperations, nums[i] <= 10^9 1<=maxOperations,nums[i]<=109
2. 解题思路
这题需要求解在操作maxOperations次内,求操作后数组内数的最大值的最小值。遇到求解最大值的最小值的问题或者最小值的最大值问题可以联想到利用二分查找。
但是二分查找需要的是有序数组,这题提供的nums是无序的,所以我们无法通过直接对nums进行查找。
我们转换一下求解问题的思路,题目要求是求解出最大值的最小值,我们不直接求解最小的值,我们从答案入手。
怎么从答案入手呢?试想一下,题目提供的 n u m s [ i ] nums[i] nums[i]是在区间 [ 1 , 1 0 9 ] [1,10^9] [1,109]内的,所以无论我们怎么把 n u m [ i ] num[i] num[i]拆分,我们最后拆分得到的数一定是在区间 [ 1 , 1 0 9 ] [1,10^9] [1,109]内的。所以,如果我们从1开始到 1 0 9 10^9 109逐一验证每一个数,看能不能在题目要求的maxOperations内得到,如果可以,那成立的最小的那个数自然就是答案了。
转换问题思路后不就能用二分查找了吗, [ 1 , 1 0 9 ] [1,10^9] [1,109]组成的数组不就是有序的吗?
但,我们每次都是从 [ 1 , 1 0 9 ] [1,10^9] [1,109]去二分吗?聪明的你肯定想到了可以从 [ 1 , m a x _ e l e m e n t ( n u m s ) ] [1,max\_element(nums)] [1,max_element(nums)]( m a x _ e l e m e n t max\_element max_element表示最大元素值)去二分查找,最终的答案肯定不会超过 n u m s nums nums中的最大值。
二分查找有套路,推荐一些学习资料,这里就直接用了,就不赘述啦~:
本题解决了转换问题这一个关键步骤后,其次最为关键的就是判断某个答案是否能在maxOperations操作次数内得到,即check函数。先说下整体思路,然后再通过几个例子讲解下check函数。
check函数思路:
- 找到nums中大于等于需要被check的数(我采取的方案是对nums排序再用lower_bound查找第一个大于等于需要被check的数的下标)
- 每个数除以被check的数,并向上取整,然后减一得到这个数需要消耗的操作数(这是check代码的核心,想一下,如果将7划分成最大数值是2,那么需要多少次?通过找规律可以发现就是 c e i l ( 7.0 / 2 ) − 1 = 3 ceil(7.0/2) - 1 = 3 ceil(7.0/2)−1=3)
- 第一次 7 -> 5, 2
- 第二次 5 -> 3, 2
- 第三次 3-> 1, 2
- 最终 7 -> 2, 2, 2, 1
- 累加所有大于等于被check的数需要操作的次数
- 判断总的操作次数并返回True or False
如果不太明白,可以先看看代码,然后再看下例子
C++ check函数
//检查是否能在maxOperations内将数分成最大值为data
bool check(int data){
// nums_global 是升序排列后的nums
// first_mt 是第一个大于等于data数的下标
int first_mt = lower_bound(nums_global.begin(), nums_global.end(), data) - nums_global.begin();
int tmp_max_operation = 0;
int len = nums_global.size();
for (int i = first_mt; i < len; ++i){
// ceil 向上取整
tmp_max_operation += (ceil(nums_global[i]/1.0/data)-1);
}
return tmp_max_operation<=max_op;
}
Python check函数
# check函数
def check(data: int, global_nums : List[int], global_maxOperations: int) -> bool:
first_mt = Solution.lower_bound(global_nums, data)
nums_len = len(global_nums)
sum_operation = 0
for i in range(first_mt, nums_len):
sum_operation += (ceil(global_nums[i]*1.0/data) - 1)
return sum_operation <= global_maxOperations
如果看了代码还不是很明白,再来看看例子:
对于 n u m s = [ 9 ] , m a x O p e r a t i o n s = 2 nums = [9], maxOperations = 2 nums=[9],maxOperations=2
- 二分查找第一步去查找5(原因可以看代码中的二分部分)
- 将 d a t a = 5 data = 5 data=5传进check函数
- 将nums升序排序后,通过用 l o w e r _ b o u n d lower\_bound lower_bound得到第一个大于等于5值的下标,即0
- 然后for循环,计算第一个数9,需要操作的次数,计算结果为1, 小于maxOperations,所以结果返回true。
- 这表示在maxOperations次数内可以将数组[9]划分成最大数为5,但是这是最小的最大数了吗?我们跟据答案知道,当然不是,所以还需要继续查找。跟据二分算法,会一直寻找,直到满足中止条件为止,这期间保存计算得到的最小的最大值。
对于 n u m s = [ 2 , 4 , 8 , 2 ] , m a x O p e r a t i o n s = 4 nums = [2,4,8,2], maxOperations = 4 nums=[2,4,8,2],maxOperations=4
- 二分查找第一步去查找4
- 将 d a t a = 4 data = 4 data=4传进check函数
- 将nums升序排序后, 通过用 l o w e r _ b o u n d lower\_bound lower_bound得到第一个大于等于4值的下标,即2
- 然后for循环,计算第一个数4需要操作的次数,很明显为0
- 然后计算下一个数8, 需要的操作数为 c e i l ( 8.0 / 4 ) − 1 = 1 ceil(8.0/4) - 1 = 1 ceil(8.0/4)−1=1。所以总的操作数为1,小于maxOperations ,所以结果返回true。
- 同样的,4依然不是最终答案,所以跟据二分算法会继续计算结果。
3. 代码
C++代码
class Solution {
public:
int max_op;
vector<int> nums_global;
int minimumSize(vector<int>& nums, int maxOperations) {
max_op = maxOperations;
sort(nums.begin(), nums.end());
nums_global = nums;
int max_value = *max_element(nums.begin(), nums.end());
// 二分查找
int left = 1;
int right = max_value;
int min_ans = 1e9+1;
while(left <= right){
int mid = left + ((right-left)>>1);
// >> 1, 表示右移1位,即除2,位运算速度快
// 如果check成立,则继续查找比他小的数
// 如果不成立则查找比他大的数,原因是结果可能是比mid大的,所以需要check比mid大的数
if (check(mid)){
min_ans = min(mid, min_ans);
right = mid-1;
}
else{
left = mid +1;
}
}
return min_ans;
}
//检查是否能在maxOperations内将数分成最大值为data
bool check(int data){
// nums_global 是升序排列后的nums
// first_mt 是第一个大于等于data数的下标
int first_mt = lower_bound(nums_global.begin(), nums_global.end(), data) - nums_global.begin();
int tmp_max_operation = 0;
int len = nums_global.size();
for (int i = first_mt; i < len; ++i){
// ceil 向上取整
tmp_max_operation += (ceil(nums_global[i]/1.0/data)-1);
}
return tmp_max_operation<=max_op;
}
};
Python代码
class Solution:
#返回nums中第一个>=target的值得位置,如果nums中都比target小,则返回len(nums)
def lower_bound(nums, target):
left, right = 0, len(nums)-1
pos = len(nums)
while left<right:
mid = left + ((right-left)>>1)
if nums[mid] < target:
left = mid+1
else:#>=
right = mid
if nums[left]>=target:
pos = left
return pos
# check函数
def check(data: int, global_nums : List[int], global_maxOperations: int) -> bool:
first_mt = Solution.lower_bound(global_nums, data)
nums_len = len(global_nums)
sum_operation = 0
for i in range(first_mt, nums_len):
sum_operation += (ceil(global_nums[i]*1.0/data) - 1)
return sum_operation <= global_maxOperations
def minimumSize(self, nums: List[int], maxOperations: int) -> int:
# sort nums
nums.sort()
max_value = nums[-1]
# 二分查找
left = 1
right = max_value
min_ans = 1e9+1
while(left <= right):
mid = left + ((right-left)>>1)
if (Solution.check(mid, nums, maxOperations)):
min_ans = min(mid, min_ans)
right = mid-1
else:
left = mid+1
return min_ans