最小化最大值是为了压制优化目标中表现最突出的成分,最大化最小值为了提升优化目标中表现最差的成分
关于这两者的理解,我觉得这篇博文讲得非常好,浅显易懂又联系实际。
理解问题后,就要思考如何解决问题。
记住,这两类问题一般都是用问题转换加二分查找的方法解决。
我会用代码+详细注释的形式记录这两类问题的解法,题目描述易于理解,耐心看完再看题解才会有收获。
最大化最小值问题:leetcode
你有一大块巧克力,它由一些甜度不完全相同的小块组成。我们用数组 sweetness 来表示每一小块的甜度。
你打算和 K 名朋友一起分享这块巧克力,所以你需要将切割 K 次才能得到 K+1 块,每一块都由一些 连续 的小块组成。
为了表现出你的慷慨,你将会吃掉 总甜度最小 的一块,并将其余几块分给你的朋友们。
请找出一个最佳的切割策略,使得你所分得的巧克力总甜度最大,并返回这个 最大总甜度。
示例 1:
输入:sweetness = [1,2,3,4,5,6,7,8,9], K = 5
输出:6
解释:你可以把巧克力分成 [1,2,3], [4,5], [6], [7], [8], [9]。
示例 2:
输入:sweetness = [5,6,7,8,9,1,2,3,4], K = 8
输出:1
解释:只有一种办法可以把巧克力分成 9 块。
示例 3:
输入:sweetness = [1,2,2,1,2,2,1,2,2], K = 2
输出:5
解释:你可以把巧克力分成 [1,2,2], [1,2,2], [1,2,2]。
提示:
0 <= K < sweetness.length <= 10^4
1 <= sweetness[i] <= 10^5
题目描述中我用下划线标注出来的语句点明了这是一道最大化最小值的问题。抛开题目情景,这道题其实可以描述为:
给定一个数组,将数组分割成K+1个连续的子数组,求一种分割方法可以使得分割后的所有子数组的和的最小值,比其他分割方法得到的子数组的和最小值都大。要求输出这个最大的最小值。
我们知道一个数组的子数组可以是它本身,也可以是一个只包含数组中任一元素的数组。所以我们可以求出数组的最小值(设为A)以及数组的和(设为B),那么我们要找的最大的最小值必定属于[A,B)。
因此,问题可以转为在[A,B)中找到一个最大的数(最大化)使得存在一种分割方法可以让所有子数组的和大于或等于这个数(最小值)。
由于[A,B)是有序的,所以在有序序列中找数,可以用二分查找。
那么如何找到上述的分割方法?
首先确定一点,这种分割方法要让所有子数组的和大于或等于某个数(设为N),那么我们就要尽量让其中的子数组的和与M的差值小一些。假设有一个子数组的和远远超过N,那就说明其他子数组可以分到的元素很少,难以保证其他子数组的和也能超过M。这就像有时候我们在分配物资一样(前提是所有物资都必须分配完),在物资充足的情况下,我们说要让每个人都至少有N件物资,结果一开始分配的时候就有一老哥上来拿走了远超过N件的物资然后就溜了,结果剩下的物资就难以保证每个人都至少有N件了,剩下的人肯定不愿意。所以一开始分配给那位老哥的时候,我们就不能让他自己拿,应该一件一件地给他,给到N件了就让他走人,这样才能保证最后大家都有至少有N件。而如果一开始物资就不足以让每个人都至少有N件,那按照这个方法分到最后肯定有人没有拿到N件物资。这样的话,我们通过记录多少人拿了N件物资,就能够区分我们手头的物资到底能不能让每个人都至少有N件。
上面的描述转换为我们讨论的数组和子数组,就是对于第一个子数组,我们依次分配给它原数组的元素直到它的和超过了N,那我们就不分配了,开始给第二个子数组分配了。最终看有多少个子数组的和大于或等于N。假设我们要求应该有T个这样的子数组,而实际上根据我们的分配得到的满足要求的子数组的个数是M。若M>T,则说明以N为最小值完全可以满足我们的要求,N甚至可以再大些;若M<T,则说明我们规定的N太大了,满足不了这样的分配,得把N设得小一点。M=T的情况可以归到M>T中,在下面的代码后会说明(结合代码说比较清楚)。
铺垫了这么多,可以写代码了:
class Solution {
public:
int maximizeSweetness(vector<int>& sweetness, int K) {
int left=100005,right=0; //在[left,right)中通过二分查找寻找我们要的值
for(int sw:sweetness){
left=min(left,sw);
right+=sw;
}
while(left<right){
//为避免出现 (left+right)/2=left,然后cancut又一直返回true的情况,采用left+right+1
int mid=(left+right+1)/2; //二分查找,mid就是我们每次给子数组的和设置的最小值
if(cancut(sweetness,K,mid)){ //cancut=true,可以根据mid来切割,试试换个大一点的mid
left=mid;
}
else{
right=mid-1;
}
}
return left;
}
//cuts:我们需要切割的次数;target:每个子数组的和要大于或等于target
//返回值:true=target设置得太小了,再大点也能满足要求;false则相反
bool cancut(vector<int>& sweetness,int cuts,int target){
int sum=0,cut=0; //sum记录子数组的和, cnt记录满足要求的子数组的个数
for(int sw:sweetness){
sum+=sw;
if(sum>=target){ //满足要求了,割它
sum=0;
++cnt;
}
if(cnt>cuts)
return true;
}
return false;
}
};
上面代码中的cancut函数为什么不考虑cnt=cuts+1的情况(即达到要求的子数组个数与题目要求的个数相同)?这种情况下的target就是我们要找的最大化最小值吗?
不是的,因为有可能数组的最后一个数组很大使得最后一个子数组的和超过2*target,那么实际上target还可以再设置为更大点。
最小化最大值问题:leetcode
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意:
数组长度 n 满足以下条件:
1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)
示例:
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
这题题目描述已经相当明确了,没有最大化最小值那题的情景设计。
我在一开始说过,这两类问题的基本思路是一样的,核心是二分查找。看了上面我的那一大段最大化最小值的解释后应该很快能类比出这题的思路。
首先明确,要求的是“最大值”,其次才是最小化的最大值。那么假设原数组中最大的元素值为A,数组的和为B,那我们要求的这个最大值一定属于[A,B),所以我们在[A,B)中二分查找。
要使最大值最小,那么在分配子数组时我们应该尽量让每个子数组足够的大。
还是拿分配物资来举例。现在我们的目标是让分配给某个人的物资不能超过K件(前提是所有物资都必须分配完)。那么如果一开始我们分配给前面的人的时候分配得很少,那么后面的人就越有可能分配到超过K件物资。因此我们要从一开始就尽可能地多分配物资,但不超过K件。
回到数组中,即我们要让第一个子数组尽可能的接近K但不超过K(可以等于,这时这个数组可能就是那个最小化最大值的子数组和了),当再分配一个元素给第一个子数组时,其和超过K,那这个元素我们不分配给它,我们分配给第二个子数组,以此类推。
若最终分割到的子数组的个数为M,而我们要求的子数组的个数为T。当M>T时,说明有太多的子数组的和接近K,我们可以让K再大一点,以减少M。当M<T时,说明接近K的子数组和太少了,我们应该让K小一点,以来更多的子数组的和接近K。
下面的代码采用long long做运算是因为不确定数据的取值范围,怕发生整数溢出。
class Solution {
public:
int splitArray(vector<int>& nums, int m) {
long long left=0,right=0;
for(int num:nums) //取最大元素值和数组和
left=max(left,(long long)num),right+=num;
while(left<right)
{
long long mid=(left+right)/2;
if(cancut(nums,m-1,mid))
right=mid;
else
left=mid+1;
}
return (int)left;
}
bool cancut(vector<int>& nums,int cuts,long long target){
long long sum=0;
int cnt=0;
for(int num:nums){
sum+=num;
if(sum>target){ //target是预设的最大值,当前子数组的和加上num之后超过了target,这是绝对不行的,所以我们在此把它割了
++cnt;
sum=num; //然后把num分配给下一个子数组
}
if(cnt>cuts) //这就是M>=T的情况,至于>=为什么可以归于一类,道理同最大化最小值最后我解释的一样
return false;
}
return true;
}
};
如果你要问怎么上面代码的二分就不用left+right+1,那你得好好温习一下二分查找了,二分查找虽然简单,但有时候自己实现起来还说不定bug重重,不断TLE。
希望从此我能很快解决这两类问题!从理解到熟记于心!