文章目录
(一) 使用场景
- 通常在有序的数字中,找到某个满足一定条件的数
- 多次问询题
- 需要多次遍历且数据量极大
(二)基本流程
- 确定边界(low, high)值
- 确定边界移动条件,一定只写一边移动(low=mid high=mid-1) 或者 (low=mid+1 high=mid)
- 循环外返回结果 (划重点)
当有多个值满足时,返回是最左边那个还是最右边那个?
def bisect_left(nums, target): # 寻找最左边的那个数,更新哪一边就是找对的最那边的数
l, r = 0, len(nums) - 1
while l < r:
m = (l + r) // 2
if nums[m] < target:
l = m + 1
else:
r = m
return l if nums[l] == target else -1
def bisect_right(nums, target): # 寻找最右边的那个数
l, r = 0, len(nums) - 1
while l < r:
m = (l + r) // 2 + 1
if nums[m] > target:
r = m - 1
else:
l = m
return l if nums[l] == target else -1
return [bisect_left(nums, target), bisect_right(nums, target)]
(三)简单例题
给定一个有序数组nums和一个整数target,判断target是否存在于nums中?
- 这种写法有陷入死循环的风险!还需改进
- (需要按照题意来确定到底是更新左边还是更新右边啊)
public boolean binary_search(int[] nums , int target) {
int low = 0;
int high = nums.length-1;
while(low<high){
int mid = low + (high-low)/2; //标准写法,避免int相加越界
if(nums[mid]<target) low = mid+1;
else high = mid; // if(nums[mid]>=target])
}
return nums[low]==target; //循环外返回结果
}
- 改进为:
public boolean binary_search(int[] nums , int target) {
int low = 0;
int high = nums.length-1;
while(low<high-1){
int mid = low + (high-low)/2; //标准写法,避免int相加越界
if(nums[mid]<target) low = mid+1;
else high = mid; // if(nums[mid]>=target])
}
return nums[low]==target or nums[high]==target; //循环外返回结果,这里多判断一次
}
(四)精选例题
(1)Leetcode 875 KoKo Eating Bananas
Koko喜欢吃香蕉,现存在N盘香蕉,每盘上有piles[i]根香蕉,总共有H个小时可以让koko进食
Koko每小时可以吃K根香蕉,但是同一小时只能选择吃一盘香蕉。
计算最小的K值,使得H小时内koko能将所有香蕉吃完。
输入: piles = [3, 6, 7, 8], H = 8
输出: K = 4
public class Eating_Bananas_875 {
public int minEatingSpeed(int[] piles, int H) {
int low = 1 , high = piles[0];
for(int i=1;i<piles.length;i++){
if(piles[i]>high) high = piles[i];
}
while(low<high){
int mid = low + (high-low)/2;
if(check(piles,mid,H)) high = mid;
else low = mid+1;
}
return low;//循环外返回
}
public boolean check(int[] piles , int speed,int H){ //最优解是low与high相遇的时候才是
int time = 0;
for(int i=0;i<piles.length;i++){
if(piles[i]%speed==0) time+=piles[i]/speed; //每次吃一盘后剩余时间不能去吃其他盘
else time += piles[i]/speed+1;
}
return time<=H;
}
}
(2) Leetcode 878 Nth Magical Number
定义一个整数是神奇的,当且仅当这个能被A或者B整除
给定A,B,返回第N个神奇数字,答案可能非常大,需要mod 10^9+7
1<=N<=10^9, 2<=A,B<=40000
输入: N = 4,A = 2,B = 3
输出: 6
解释:2,3,4,6
public class Nth_Magical_Number_878 {
public int nthMagicalNumber(int N, int A, int B) {
long low=2L,high=100000000000000L; //上界到底怎么算呢?随便找个最大值试一下
while(low<high){
long mid = low + (high-low)/2;
if(check(mid,N,A,B)) low = mid+1;
else high = mid;
}
return (int)(low%1000000007); //少加了括号导致WA了
}
// 检查Mid是第多少个神奇数
// mid / A 是A的倍数,也就是Mid之前有多少个满足能被A整除的数
// mid / B 同理,但是中间可能有既是A的倍数也是B的倍数的,所以要把这些重复值删除
public boolean check(long mid,int N,int A,int B){
long magicals = mid/A + mid/B - mid/lcm(A,B);
return magicals < N ;
}
public int gcd(int a,int b){ //最大公约数
return b==0?a:gcd(b,a%b);
}
public int lcm(int a,int b){ //最小公倍数
return a*b/gcd(a,b);
}
}
(3) Leetcode 778 Swim in Rising Water
给定一个NxN的网格grid,请你找到一条道路,从(0,0)出发一直走到(n-1,n-1),使得这条路上的最大值最小。并返回这个值。
class Solution {
public int swimInWater(int[][] grid) {
if(grid==null || grid.length==0 || grid[0].length==0) return 0;
int len = grid[0].length;
int low=grid[0][0],high=len*len-1;
while(low<high){
int mid = low + (high-low)/2;
boolean[][] visited = new boolean[len+1][len+1];
if(DFS(mid,0,0,grid,visited)==1) high = mid;
else low = mid+1;
}
return low;
}
public int DFS(int depth,int x,int y,int[][] grid,boolean[][] visited) //在点(x,y)出发能找到一条路径,路径上的值都小于mid
{
int n = grid[0].length;
if(x<0 || x>=n || y<0 || y>=n)
return 0;
if(visited[x][y] || grid[x][y]>depth) //空指针异常,visited数组有问题
return 0;
if(x==n-1&&y==n-1)
return 1;
int[][] DIRS = new int[][]{{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
visited[x][y]=true; //标记(x,y)已经访问过了
for(int[] dir:DIRS){
int newX = x + dir[0];
int newY = y + dir[1];
if(DFS(depth,newX,newY,grid,visited)==1)
return 1;
}
return 0;
}
}