10.5二分专练,二分边界情况,+1不加1的判断,方向判断,各种DEBUG

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;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值