例题1: 你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
理解这道题是通过不断缩小边界然后当左右边界重合是就是答案,至于要不要取等号,这里推荐不要取等号尽管取等号也可以将题目解出
/* The isBadVersion API is defined in the parent class VersionControl.
boolean isBadVersion(int version); */
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
// 使用迭代超出了时间限制,那就二分查找
int left=1;
int right=n;
while(left<right){
// 首先取中间值这里为了防止溢出整数的范围,这里先将
int mid =left+(right-left)/2;
// 下面开始缩小范围
if(isBadVersion(mid)){
right=mid;
}else{
left=mid+1;
}
}
return left;
}
}
例题2:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。(力扣:35)
public class Solution {
public int searchInsert(int[] nums, int target) {
int len = nums.length;
int left = 0;
int right = len;
// 在区间 nums[left..right] 里查找第 1 个大于等于 target 的元素的下标
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target){
// 下一轮搜索的区间是 [mid + 1..right]
left = mid + 1;
} else {
// 下一轮搜索的区间是 [left..mid]
right = mid;
}
}
return left;
}
}
二分法的最新理解:
我们要判断进行边界收缩时,条件成立时移动的时左指针还是右指针,如果时移动做指针时,需要判断边界条件会不会卡死,就例如下面的代码:想象一下当只剩下4,5,如果int mid=(r+l)/2,那么当条件成立mid就永远为时会出现死循环,所以当移动左边界时,需要注意,移动右边界就不需要考虑了因为肯定会变小的
while(l<r) {
// 这里为什么要加一,因为如果一旦条件成同时有只剩下两个,也就是说mid就不再变化了,所以会导致死循环
int mid =(r+l+1)/2;
if(check(mid,arr,K)) {
l=mid;
}else {
r=mid-1;
}
}
例题3:
package four;
import java.util.Scanner;
//这道题我本身是没有什么思路的,但是题解是使用2分法找出
//最大的边长
public class Four_2 {
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int N=scan.nextInt();
int K=scan.nextInt();
// 创建一个2维数组保存每块巧克力的信息
int[][] arr=new int[N][2];
for(int i=0;i<N;i++) {
for(int j=0;j<2;j++) {
arr[i][j]=scan.nextInt();
}
}
// 利用二分法来获取最大的边长
int l=1;
int r=100000;
while(l<r) {
// 这里为什么要加一,因为如果一旦条件成同时有只剩下两个,也就是说mid就不再变化了,所以会导致死循环
int mid =(r+l+1)/2;
if(check(mid,arr,K)) {
l=mid;
}else {
r=mid-1;
}
}
System.out.print(l);
}
// 创建一个函数以当前长度去分割巧克力是否可行
// 判断的条件就是分割后的巧克力总和能否大于人数
public static boolean check(int maxlen,int[][] arr,int k) {
// 创建一个变量计算总和
int sum=0;
for(int i=0;i<arr.length;i++) {
int count=(arr[i][0]/maxlen)*(arr[i][1]/maxlen);
sum=sum+count;
}
return sum>=k;
}
}
例题:p1873 砍树问题
理解:这道题根之前的一道题一样都容易造成死循环,所以在求mid的时候需要时mid=(left+right+1)/2;而不是(left+right)/2
package 每天打卡;
import java.util.Arrays;
import java.util.Scanner;
//思路使用标签提示的二分法,对每个中点的值都进行计算
public class P1873 {
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int N=scan.nextInt();
int M=scan.nextInt();
int max=0;
// 创建一个数组保存结果
int[] nums=new int[N];
for(int i=0;i<N;i++) {
nums[i]=scan.nextInt();
if(nums[i]>max) {
max=nums[i];
}
}
// 创建两个指针
int left=0;
int right=max;
while(left<right) {
//注意上面这个循环的条件;当left==right,跳出循环也就找到了满足条件的最小高度
int mid =(left+right+1)>>1;
// 如果当前切割的高度过于小
if(sum(nums,mid)>=M) {
//上面不能用mid=(left+right)/2! 因为当sum()为>+m,那么每次的mid都是left,结果left和right的值都没有改变,造成死循环
//换句话说,mid要偏向right,就是使切割树的高度变高
left=mid;
}else {
right=mid-1;
}
}
// 输出结果
System.out.print(right);
}
// 创建一个函数求以当前高度切割能获取的高度
// 必须使用一个长整型来保存结果,不然可能会因为int装不下而出错
public static long sum(int[] nums,int mid) {
long total=0;
for(int i=0;i<nums.length;i++) {
if(nums[i]>mid) {
total=nums[i]-mid+total;
}
}
return total;
}
}
例题:木材加工
import java.util.Scanner;
public class Main{
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int n=scan.nextInt();
int k=scan.nextInt();
int[] nums=new int[n];
int max=0;
for(int i=0;i<n;i++) {
nums[i]=scan.nextInt();
if(nums[i]>max) {
max=nums[i];
}
}
int left=0;
int right=max;
// 使用二分法来查找满足k的最大
while(left<right) {
int mid=(left+1+right)/2;
if(pd(nums,k,mid)) {
left=mid;
}else {
right=mid-1;
}
}
// 输出结果
System.out.print(left);
}
public static boolean pd(int[] nums,int k,int cutlen) {
// 创建一个变量保存切割的段数
int count=0;
for(int i=0;i<nums.length;i++) {
count=count+nums[i]/cutlen;
}
return count>=k;
}
}