力扣刷题记录---二分法

一般的二分查找应用的地方都是在一个单调有序序列里面进行值的搜索,,用中间点进行区域划分,当中间值大于目标值target,说明目标值在左区域,反之则在右区域。这样不断缩小区域,每次搜索区域都只要当前范围的一半,所以称为二分法。

但是并不是只有单调的序列才能使用二分法,二分的本质是边界的划分,只要集合/序列上有这么一种性质,这个性质能够在当前区域进行区域划分,使得左区域满足,但右区域不满足;或者是左区域不满足,右区域满足。二分法就是一种可以找到上述区域边界的方法。

整数二分

因为整数进行区间划分时,如果边界没确定好,容易造成死循环,所以需要更特别的考虑,根据不同的边界划分方式,有两种二分法的代码模板:

版本1
当我们将区间[l, r]划分成[l, mid - 1]和[mid, r]时,其更新操作是r = mid - 1或者l = mid;,此时为了防止死循环,计算mid时需要加1。

C++ 代码模板:

int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

版本2
当我们将区间[l, r]划分成[l, mid]和[mid + 1, r]时,其更新操作是r = mid或者l = mid + 1;,计算mid时不需要加1。

C++ 代码模板:

int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    return l;
}

选择模板的流程如下图:
在这里插入图片描述

mid是等于(l+r+1)/2还是等于(l+r)/2其实是取决于if(check(mid))为true时,是l=mid还是r=mid。

  • if(check(mid))l=mid时(即check(mid)=true),前面的mid应当为(l+r+1)/2,因为(l+r)/2是向下取整,l=mid时,如果l和r的下标只相差1,那么mid=(l+r)/2=l,在这种情况下,下一个二分区间[mid,r]–>[l,r],会重复进入[l,r]区间,造成死循环,所以l=mid这样的代码模板,需要使mid=(l+r+1)/2
  • check(mid)=true时,是采用l=mid还是r=mid,就可以区分采用何种代码模板了,check(mid)=false时,只要根据前面的区间划分进行另一个区间的边界赋值即可。比如:check(mid)=true时,l=mid,是使区间从[l,r]-->[mid,r],那么check(mid)=false时,就要进入左区间,即r=mid-1,使区间从[l,r]-->[l,mid-1]

这里还有一篇文章对二分需要注意的一些细节进行了总结,可以参考:二分法的细节


AcWing 789. 数的范围

原题链接

在这里插入图片描述

这题因为是找目标元素x在升序数组q中的起始和终止坐标,可以比较好的应用二分法的两种代码模板。

代码如下:

import java.util.*;

public class Main{
    
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int m=sc.nextInt();
        int[] q=new int[n];
        for(int i=0;i<n;i++)q[i]=sc.nextInt();
        
        //m个询问
        while(m-->0){
            int x=sc.nextInt();
            
            int l=0,r=n-1;
            while(l<r){
                int mid=l+r>>1;
                //寻找x的起始坐标,当中间元素大于等于x
                //说明x的起始索引在中间点左边(包括q[mid]),下次范围限定在左区间
                if(q[mid]>=x)r=mid;//选择模板2
                else l=mid+1;
            }
            //也可能找不到x,这时候就输出-1,-1
            if(q[l]!=x)System.out.println("-1 -1");
            else{//找到了x的起始坐标后,就要找终止坐标了
                System.out.print(l+" ");
                l=0;r=n-1;
                while(l<r){
                    int mid=l+r+1>>1;
                    //寻找x的终止坐标,中间元素小于等于x
                    //说明x的终止坐标在中间点右边(包括q[mid]),下次范围限定在右区间
                    if(q[mid]<=x)l=mid;//提示采用模板1
                    else r=mid-1;
                }
                System.out.println(l);
            }
            
        }
        
    }
}

LeetCode 1201. 丑数 III

原题链接

在这里插入图片描述

主要思路:设计一个函数count(num,a,b,c),功能是计算1~num之间有多少个数能被a或b或c整除,计算“1-num之间能被a整除的数的个数”是num/a,同理b、c是num/b,num/c,但是“能被a或b或c整除”的数不能将它们简单地进行相加,这里面涉及容斥原理:
在这里插入图片描述

在1-num区间,设能被a整除的数的集合为集合setA,同理能被b整除的数的集合为setB,能被c整除的为setC,既能被a也能被b整除的数的集合为setAB,同理有setAC、setBC、setABC。

容斥原理可得:能被a或b或c整除的个数setA+setB+setC-setAB-setAC-setBC+setABC

显然这个函数count的返回值会随着num的增加而单调递增,符合二分法适用范围,只要检查count的返回值与n 的关系,当count(num,a,b,c)<n时,说明1~num的范围中丑数的个数还不够,需要向右区间搜索,反之则向左区间搜索。二分范围依题意可以缩小到l=Math.min(a,Math.min(b,c)),r=min*1L*n+1;

确定采用二分法之后,需要解决的是如何计算能被整除的数的个数,setA、setB、setC的求法已知为num/a,setAB表示同时可以被a和b整除的元素个数,也就是A∩B,A ∩ B 的元素个数就是 n / lcm(a, b),其中 lcm 是计算最小公倍数(Least Common Multiple)的函数,类似的,A ∩ B ∩ C 的元素个数就是 n / lcm(lcm(a, b), c) 的值。

那么最小公倍数怎么求呢?

定理:lcm(a, b) = a * b / gcd(a, b),其中 gcd 是计算最大公因数(Greatest Common Divisor)的函数。

那么最大公因数呢?可以采用辗转相除法(欧几里得算法),具体思想可以自行百度。

代码如下:

class Solution {
    public int nthUglyNumber(int n, int a, int b, int c) {
        
        int min=Math.min(a,Math.min(b,c));
        long l=min,r=min*1L*n+1;//二分初始区间优化
        while(l<r){
            long mid=l+r>>1;//防止爆int
            if(count(mid,a,b,c)<n)l=mid+1;//说明mid不符合,要在mid右边开始找
            else r=mid;
        }
        return (int)l;
    }

    //计算1~num中有多少个数能被a、b、c整除(容斥原理)
    private long count(long num,long a,long b,long c){
        long setA=num/a,setB=num/b,setC=num/c;
        long setAB=num/lcm(a,b);
        long setAC=num/lcm(a,c);
        long setBC=num/lcm(b,c);
        long setABC=num/lcm(a,lcm(b,c));
        return setA+setB+setC-setAB-setAC-setBC+setABC;
    }

    //求最小公倍数(Least Common Multiple)
    private long lcm(long a,long b){
        return a*b/gcd(a,b);
    }

    //求最大公因数(Greatest CommonDivisor)--辗转相除法
    private long gcd(long a,long b){
        long big=Math.max(a,b);
        long small=Math.min(a,b);
        if(small==0)return big;
        return gcd(small,big%small);
    }

    
}

LeetCode 878. 第 N 个神奇数字

原题链接

在这里插入图片描述

弄懂了LeetCode 1201. 丑数 III,再来做这题就很容易了,其实就是1201题的简易版本。

代码如下:

//时间复杂度O(log(min(a,b)*n))
class Solution {
    public int nthMagicalNumber(int n, int a, int b) {
        //所有取long类型的都是为了防止爆int
        long l=Math.min(a,b),r=l*n;
        while(l<r){
            long mid=l+r>>1;
            if(count(mid,a,b)<n)l=mid+1;
            else r=mid;
        }
        return (int)(r%(1e9+7));
    }

    long count(long num,int a,int b){
        return num/a+num/b-num/lcm(a,b);
    }

    int lcm(int a,int b){
        return a*b/gcd(a,b);
    }

    int gcd(int a,int b){
        int big=Math.max(a,b);
        int small=Math.min(a,b);
        if(small==0)return big;
        return gcd(small,big%small);
    }


}

LeetCode 69. x 的平方根

原题链接

可以采用二分法去逼近那个整数,需要注意的是,需要舍去小数部分,相当于优先找左边界。

代码如下:

class Solution {
    public int mySqrt(int x) {
        if(x==0)return 0;
        int l=1,r=x;
        while(l<r){
            int mid=l+(r-l+1)/2;
            if(mid<=x/mid)l=mid;
            else if(mid>x/mid)r=mid-1;
        }
        return l;
    }
}

LeetCode 367. 有效的完全平方数

原题链接

代码如下:

class Solution {
    public boolean isPerfectSquare(int num) {
        int l=1,r=num/2+1;
        while(l<r){
            int mid=l+(r-l)/2;
            long square=(long)mid*mid;
            //里面如果写成mid<num/mid这样形式,num/mid会导致向下取整
            //比如5/2=2,这样就会导致当mid为2时,无法满足2<5/2
            //而是满足2=5/2=2,直接return true,造成错误
            if(square<num)l=mid+1;
            else if(square>num)r=mid-1;
            else return true;
        }//
        //退出时l=r,最后一次mid*mid与num的关系还没检查过,需要补充
        if(l*l==num)return true;
        return false;
    }
}

浮点数二分

AcWing 790. 数的三次方根

原题链接
在这里插入图片描述

浮点数的二分法

代码如下:

import java.util.*;
public class Main{
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        double n=sc.nextDouble();
        double l=-10000,r=10000;
        while(r-l>=1e-8){
            double mid=(l+r)/2;
            if(mid*mid*mid>=n)r=mid;
            else l=mid;
        }
        
        System.out.printf("%.6f",l);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,关于力扣刷C++常用操作,我可以给你一些常见的操作和技巧: 1. 使用 STL 容器和算法库:STL(Standard Template Library)是 C++ 标准库中的一个重要组成部分,包含了许多常用的容器和算法。在力扣刷中,使用 STL 可以大大提高代码的效率和可读性。例如,vector 可以用来存储动态数组,sort 可以用来排序等等。 2. 使用 auto 关键字:auto 关键字可以自动推导变量类型,可以减少代码量和提高可读性。例如,auto x = 1; 可以自动推导出 x 的类型为 int。 3. 使用 lambda 表达式:lambda 表达式是 C++11 中引入的一种匿名函数,可以方便地定义一些简单的函数对象。在力扣刷中,使用 lambda 表达式可以简化代码,例如在 sort 函数中自定义比较函数。 4. 使用位运算:位运算是一种高效的运算方式,在力扣刷中经常会用到。例如,左移运算符 << 可以用来计算 2 的幂次方,右移运算符 >> 可以用来除以 2 等等。 5. 使用递归:递归是一种常见的算法思想,在力扣刷中也经常会用到。例如,二叉树的遍历、链表的反转等等。 6. 使用 STL 中的 priority_queue:priority_queue 是 STL 中的一个容器,可以用来实现堆。在力扣刷中,使用 priority_queue 可以方便地实现一些需要维护最大值或最小值的算法。 7. 使用 STL 中的 unordered_map:unordered_map 是 STL 中的一个容器,可以用来实现哈希表。在力扣刷中,使用 unordered_map 可以方便地实现一些需要快速查找和插入的算法。 8. 使用 STL 中的 string:string 是 STL 中的一个容器,可以用来存储字符串。在力扣刷中,使用 string 可以方便地处理字符串相关的问。 9. 注意边界条件:在力扣刷中,边界条件往往是解决问的关键。需要仔细分析目,考虑各种边界情况,避免出现错误。 10. 注意时间复杂度:在力扣刷中,时间复杂度往往是评判代码优劣的重要指标。需要仔细分析算法的时间复杂度,并尽可能优化代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值