5
https://leetcode.cn/problems/minimum-speed-to-arrive-on-time/submissions/570242512/
就是说总时间是
前n-1量汽车的运行时间,向上取整,然后再加上最后一辆列车的运行时间
最快的话是需要n-1个小时
搜索空间就是时速,左边界是1,右边界是其中最大值
当为最大值时,就是n-1再加上最后的距离与该时速的比值
如果大于目标时间hour,说明不满足条件,应当增大速度,即向右侧找,同时舍去该解,left=mid+1
如果小于等于hour,说明满足条件,可以去找左侧的最小值,right=mid
不应该加1
class Solution {
public:int minSpeedOnTime(vector<int>& dist, double hour) {int leftB=1;int rightB=10e7;// for(int i:dist){// if(i>rightB){// rightB=i;// }// }if(dist.size()>=hour+1){return -1;}int mid=0;while(leftB<rightB){int tarN=0;mid=(leftB+rightB)/2;for(int i=0;i<dist.size()-1;i++){tarN+=((dist[i]/mid)+((dist[i]%mid)?1:0));}double tar=double(dist[dist.size()-1])/double(mid);// cout<<"预期的小时部分"<<tar<<endl;tar+=tarN;if(tar>hour){// cout<<"此时左边界为"<<leftB<<"右边界为"<<rightB<<"速度为"<<mid<<"总用时是"<<tar<<"前n-1量用时为"<<tarN<<endl;leftB=mid+1;}else{//cout<<"此时左边界为"<<leftB<<"右边界为"<<rightB<<"速度为"<<mid<<"总用时是"<<tar<<"前n-1量用时为"<<tarN<<endl;rightB=mid;}}mid=(leftB+rightB)/2;return mid;}
};
优化
对初始的上界与下界进行优化,下界至少是1的,但是更好情况是路程总和与要求时间的平均,或者说这个就是最好的期望解,即每段都是相同的,
对于上界,没看懂它的优化,先放在这里
l = max(1, int(sum(dist)//hour))
r = int(sum(dist)//(hour-n+1) + 1)
但是要注意,下界在用平均的时候,务必要增加一个与1的Max判断,就是为了防止0出现在搜索空间当中
而且在算累和的时候,务必要注意数据范围,一般应当要用Longlong来记录累和,用Int的话必爆
在用上平均值优化后,优化效果并不是很好\\,甚至还有点拉托了
Debug
限制值错误
第一次是因为忽略了非整数部分的取值,即没有向上取整,即
tarN+=(dist[i]/mid);
改为
tarN+=((dist[i]/mid)+((dist[i]%mid)?1:0));
即可
上界取值错误
这是因为一开始认为速度右侧最大值是里程里的最大值,但是如果它是在最后的话,完全可以使速度达到一个很大的值,从而使最后一段的速度降下来
这个问题,个人目前没有很好的解决办法
参考官方题解:
由于时限最小单位是0.01,最大的距离是 1 0 5 10^5 105,所以在最后一段,极限的最快速度就是 1 0 7 10^7 107,如果再快的话,就会导致时间小于0.01了,所以时间上限就是 1 0 7 10^7 107
小数除法
这次错是因为最后一段的小数部分,由于是让整数之间进行相除,即
double tar=(dist[dist.size()-1])/(mid);
会导致tar虽然是double类型,但根本不会产生小数,只有把分子分母都强转为double,才可以使tar为小数,即
double tar=double(dist[dist.size()-1])/double(mid);
4
https://leetcode.cn/problems/minimized-maximum-of-products-distributed-to-any-store/
就是去搜索每个店被分到的商品最大值x,
左边界是1,右边界是其中的最大值
x越大,那么分出的商店数越少;
限制条件是商店数,如果x对应到的商店数k,
大于给定的商店数n,则说明不满足条件,应当向减少商店数的方向去搜索,即x大、右侧去搜索,left=mid+1
小于等于给定的商店数,说明满足条件,尝试增大商店数,即向左侧去搜索,right=mid,并保留该解
只剩两个值的时候,应首先向左侧去搜索,所以不加1
一遍过
class Solution {
public:int minimizedMaximum(int n, vector<int>& quantities) {int leftB=1;int rightB=quantities[0];for(int i:quantities){if(i>rightB){rightB=i;}}if(n<=quantities.size()){return rightB;}int mid;while(leftB<rightB){int tar=0;mid=(leftB+rightB)/2;for(int i:quantities){tar+=((i/mid)+((i%mid)?1:0));}if(tar>n){leftB=mid+1;}else{rightB=mid;}}mid=(leftB+rightB)/2;return mid;}
};
3
https://leetcode.cn/problems/minimum-limit-of-balls-in-a-bag/description/
歧途——贪心大顶堆
一开始想法就是维护一个最大堆,每次都取堆顶元素,然后给他除以2,如此循环操作给定的次数,最后堆顶就是代价
之所以要除以2,是因为如果不这样的话,必然会有比2还大的数在里面,相当于没整
考虑到不能整除的问题,每次加入就是加入(num)/2与(num+1)/2,这样就是如果能整除,加的也是两个相同的数;不能整除,就是一个左值,一个右值
class Solution {
public:int minimumSize(vector<int>& nums, int maxOperations) {priority_queue<int>p;for(int i=0;i<nums.size();i++){p.push(nums[i]);}for(int i=1;i<=maxOperations;i++){int num=p.top();p.pop();p.push(num/2);p.push((num+1)/2);}return p.top();}
};
问题
考虑这种情况,9-》4和5,5再拆分成2和3,导致结果是4,但如果一开始分成3和6,第二次拆成3,3,3,就会使最后结果为3,贪心确实行不通
贪心行不通的原因
参照题解,贪心的思路无非是在处于2y与3y之间的值,使其变为y与2y之间的值,那么要让原值完全处于y之下,则需要3次操作,而如果是变为<=y与y与2Y之间的两个数,则只需要2次操作
也就是逆向思维:不去向怎么从原始值变出最优解,而是先假定一个最优解,然后看各个值怎么达到这个最优解
而问题也就出现在上面的贪心思路到这个最优解的路径上了
二分
如果卡死一个最优解的线,每个袋子要达到这个最优解,必然要有一个策略
就是对于含有x个东西的袋子,怎么拆分才能使其产生的袋子都达到y以下
显然也应当逆向思维,每次拆分相当于多了一个袋子,最后不就是把x个东西分到k个袋子当中,每个袋子里装的数量不超过y吗?
这样的话最优解必然也是每个袋子里装的都是y
而按照其它划分方法,尤其是上面的贪心,每次分一半的话,很有可能多用
就是把问题看成,一共有x个东西,每个袋子只能装y个,问一共需要多少个袋子的问题
那么每个袋子能装的最大值是数组里的最大值,最小值是1,
限制条件就是切分的次数,即多用的袋子的数量o
如果袋子容量对应的切分数大于o的话,说明不满足条件,应当要减少切分次数,即扩大每个袋子的容量以减少袋子所用的数量
如果小于等于o,说明满足条件,保留该解,并尝试缩小袋子容量,即向左侧找,right=mid,从而减小开销
对于Mid,由于满足条件时,是尝试向左取值,所以不应当+1;
搜索空间就是袋子所能放的数量
class Solution {
public:int minimumSize(vector<int>& nums, int maxOperations) {int leftB=1;int rightB=nums[0];for(int i:nums){if(i>rightB){rightB=i;}}int mid=1;while(leftB<rightB){int sum=0;mid=(leftB+rightB)/2;for(int i:nums){sum+=(i/mid)+((i%mid)?0:-1);}if(sum>maxOperations){cout<<"此时左边界为"<<leftB<<"右边界为"<<rightB<<"袋子装的数量为"<<mid<<"切分数是"<<sum<<endl;leftB=mid+1;}else{cout<<"此时左边界为"<<leftB<<"右边界为"<<rightB<<"袋子装的数量为"<<mid<<"切分数是"<<sum<<endl;rightB=mid;}}mid=(leftB+rightB)/2;return mid;}
};
DEBUG
端点情况
考虑这个切分,就是想象成一个线段,切分成k段不大于Y的子段的次数
如果刚好达到端点的情况,就不应当再继续分
即将 sum+=(i/mid)
改为sum+=(i/mid)+((i%mid)?0:-1);
2
https://leetcode.cn/problems/koko-eating-bananas/
最大值肯定是香蕉堆里的最大值
最小值期望是香蕉总数除以小时数
如果堆数大于小时数,则必然无解
速度越快,那么小时数越小,但不是规则变动的
满足条件是要让小时数和给定的去比较
也是从左到右是递减的
如果小时数大于给定的话,就是不满足条件,应当向右侧去找
如果小于的话,是满足条件,应当向左侧去找代价更小的
class Solution {
public:int minEatingSpeed(vector<int>& piles, int h) {int rightB=piles[0];long long sum=0;for(int i:piles){sum+=i;if(i>=rightB){rightB=i;}}int leftB=sum/h;//int leftB=((sum/h)?sum/h:1);if(piles.size()>=h){return rightB;}if(leftB==0){return 1;}int mid=0;// cout<<"此时左边界为"<<leftB<<"右边界为"<<rightB<<"每小时吃的香蕉数为"<<mid<<endl;while(leftB<rightB){long long tar=0;mid=(leftB+rightB)/2;for(int i:piles){tar+=((i/mid)+((i%mid)?1:0));}if(tar>h){// cout<<"此时左边界为"<<leftB<<"右边界为"<<rightB<<"每小时吃的香蕉数为"<<mid<<"小时数是"<<tar<<endl;leftB=mid+1;}else{//cout<<"此时左边界为"<<leftB<<"右边界为"<<rightB<<"每小时吃的香蕉数为"<<mid<<"小时数是"<<tar<<endl;rightB=mid;}}mid=(leftB+rightB)/2;return mid;}
};
DEBUG
1
剪枝判断写反了,应当是堆数大于小时数时,就退出,写成如果堆数小于时就退出
2
第二个是二分判断条件写错了,应当是tar>h是不满足条件的,写成了tar>=h,导致会丢掉可行解,而丢掉的可能正是最优解
=的时候是满足条件的,应当保留
除零错误
期望的最小左值一开始设定的是sum/h,如果sum<h的时候,就会使左值为0,
sum<h就是说每小时只吃一个都戳戳有余,如果保留了左值0,那么1肯定满足条件,但是会接着去尝试为0的情况,从而导致了错误
两种解决方法,一种是规定左值最小值为1,另一种就是提前剪枝,如果左值为0,就直接返回1即可
就分析性能而言,应当是提前剪枝的效果更好
3(Mid+1)判断?
产生了死循环
就是tar=h的情况,满足条件且只剩两个值的情况,是保留缩减右侧,尝试继续向左边找结果,那么mid就不应该+1,因为+1就是向右侧去尝试,
而只剩两个值的时候,应当尝试一下左边的,如果继续尝试右边的,就会陷入right=mid,保持死循环;而如果尝试一下左边的,满足条件,就会使right=mid,动了右侧;不满足,就是Left=mid+1.动了左侧,从而避免了死循环
如果是+1的话,就是在除法取到0.5的时候去向上取整
如果不+1,就是有0.5时向下取整
这个取整方向应该和搜索范围缩减的方向相匹配,不然就会导致死循环
如果不满足条件时是left+1,mid是向增大的方向增加,说明向右是满足条件的,此时或许应该不该+1,即尝试左端的left是否满足条件,否则可能会
看只剩最后两个值时的情况,即到底是保留左值还是右值,如果+1就是保留左值,不加就是保留右值
如果陷入了死循环,在+1的情况,说明是去取了右值,
陷入死循环说明此时的值肯定是满足条件的,但是取值的方向和满足条件的方向保持了一致
总结一下就是考虑只剩两个值时候的取值倾向,满足条件时,到底是继续向左还是向右去找值,如果是向左找值,就不应当+1;如果是向右找值,就应当+1;
这个题满足条件就是小时数<=h,速度越快则小时数越小,在满足条件时,应当向左侧找值,所以就不应该+1
1
https://leetcode.cn/problems/maximum-candies-allocated-to-k-children/description/
k个小孩,每个小孩要的糖果数量要一样,如果是n,那么糖果总数就是nk
如果糖果总数和sum小于nk,则必然无解
最好的情况必然是sum/k,是每个小孩能平分到的最大糖果数
每堆数量设为x,
搜索空间是每个小孩分的糖果数,每次搜索的判断是这个糖果数对应出来的堆数
显然每个小孩分的糖果数越多,那么分出来的堆数是越小的,
目标值在搜索空间的从左到右是递减的
那么这样的话,如果搜索值对应到的堆数是比k小的,那么应当是尝试去左侧去找,即让分的糖果数变少,从而增大堆数
目的是要搜索空间的右值,越向右则分的糖果数越多,堆数越少
如果某个区间右值对应中值出来的堆数满足要求,那么还是应当往右靠
如果不满足的话,说明不够,此时应当往左靠
堆数小于k时,应当去
DEBUG
1
第一个错是因为没有考虑如果一开始左边界和右边界就是0的情况,这样的话就不会开启循环,那最后输出的mid,就是没有值的,一个随机值
解决方法
就是在一开始的时候加一个判断,如果左和右都是0的话,直接退出,不返回Mid了,或着让mid一开始就为0,反正也不会进入while循环里,mid也不会被设新值
2
第二个是因为再次写的时候的一个小聪明,即while退出的条件从leftB<=rightB改为leftB<rightB,考虑的是==的时候必然是要退出循环的,但是<=的时候,在循环内部判断的话,可以给mid赋新值,是只有这个值对应的Mid的值
而如果是<的话,当相等的时候,是直接退出while循环的,而mid没有更新,还是之前的旧值
解决方法
要么最后的时候,即退出while循环后mid再一个赋值等式
要么还是写为<=,在while里判断是否相等
但是性能的话应该是第一种好,因为<=的话会让每次循环都多一个if判断
3
一个致命错误是算堆数的时候是让mid/i,但应该是让i/mid
4
一个致命错误是惯性思维,还是按着如果满足要求就保留右边界right=mid,不满足要求就让left=mid+1的方向去写
但这种写法是只适用于在搜索空间中从左到右,目的值是递增的情况
因为>=k是目的值满足条件的,此时right=mid,相当于在满足条件的基础上进一步缩小搜索空间,以寻找代价更小的解
而<k是不满足条件的,此时需要向满足条件的方向去寻找,left=mid+1就是去剔除掉左侧不满足条件的所有解与mid这个解
当从左到右,目的值是递减的情况时,如果满足条件还保留right=mid,虽然此时的mid时满足条件的,但是相当于自行截掉了右侧所有可能依然满足条件且代价更小的解
如果不满足条件让left=mid+1,则更是荒谬,因为此时mid都不满足了,继续向右找,更是完全找不到
相反,此时应当是满足条件时,保留左侧的这个可行解,而尝试向右侧去找代价更小的解,即满足条件时left=mid,不满足时,向左侧去找,right=mid-1
5
又一个致命错误是死循环的产生
在这种样例下,满足条件,保留left不动,那么left一直不动,right也不动,从而造成死循环
在从左到右目的值是递增情况下不会出现这种情况,是因为除法是向下取整的
而这种情况下,由于是向下取整,满足时还left不动,就会导致最后区间只有两个相邻的值,但是/完后是0.5,不会进1,导致一直取下值,造成死循环
改
中值写为
mid=(leftB+rightB+1)/2
class Solution {
public:int maximumCandies(vector<int>& candies, long long k) {long long sum=0;for(int i:candies){sum+=i;}int leftB=0;int rightB=sum/k;int mid=0;while(leftB<rightB){long long res=0;mid=(leftB+rightB+1)/2;for(int i:candies){res+=(i/mid);}if(res<k){cout<<"此时左边界为"<<leftB<<"右边界为"<<rightB<<"糖果数为"<<mid<<"分的堆数是"<<res<<endl;rightB=mid-1;}else{cout<<"此时左边界为"<<leftB<<"右边界为"<<rightB<<"糖果数为"<<mid<<"分的堆数是"<<res<<endl;leftB=mid;}}mid=(leftB+rightB)/2;return mid;}
};