算法总结-二分法

前言

  二分法的题单可以刷 灵茶山艾府 的题单分享丨【题单】二分算法(二分答案/最小化最大值/最大化最小值/第K小)。本文是该分享的学习笔记。

  二分法的核心思想在于利用 有序性 ,通过反复将搜索区间 一分为二 ,逐步缩小查找范围,从而提高查找效率。
  二分法的使用场景:待查找的数据必须是有序的

二分查找

算法思路

  1.定义左右指针left, right,表示一个区间 QQ 区间表示目标 target 在这个区间范围内。
  2.当区间 Q 内还有元素时,不断移动 leftright 来缩小区间 Q 的范围,直到区间 Q 没有元素,退出循环。
  3.需要注意每次移动 leftright 一定要使 Q 能够缩小,否则会陷入死循环。
  4.根据移动 leftright 时书写的条件,判断当循环结束后 leftright 所表示的具体含义,根据这个具体含义来确定最后返回的到底是什么值。

二分法基本模板

//找到nums中第一个大于等于target的元素的下标
int lower_bound(vector<int>& nums, int target){
	定义 left,right 表示区间 Q
	while(区间Q中有元素){
		int mid = left + (right-left)/2;
		if(nums[mid]在target右边){
			移动right
		}
		else{
			移动left
		}
	}
	返回nums中第一个大于等于target的下标
}

闭区间模板

//找到nums中第一个大于等于target的元素的下标
int lower_bound(vector<int>& nums, int target){
	int n = nums.size();
    int left = 0, right = n-1; 
    while(left<=right){
        int mid = left + (right-left)/2;  //防止整型溢出
        if(nums[mid]>=target){  
            right = mid-1;  
        }
        else{
            left = mid+1; 
        }
    }
    return left; 
}

解释:

  1. 使用 [left, right] 来表示区间 Q, 初始时,Q 为整个数组区间 [0,n-1],因此定义 left = 0 , right = n-1 表示初始时的区间 Q

  2. left <= right 时,区间 [left, right] 还有元素,因此,此时 while循环 里的条件是 left <= right

  3. right 更新时不能更新为 mid,因为当 left = rightnums[mid] = target 时,mid = right, 则 right 的更新不会缩小 Q 区间,造成死循环。left 类似。

  4. 在一次移动 right 的时候,设移动后的 right 值为 right1,则 right1 = mid-1, 那么有:
    n u m s [ r i g h t 1 + 1 ] = n u m s [ m i d − 1 + 1 ] = n u m s [ m i d ] nums[right1+1]=nums[mid-1+1]=nums[mid] nums[right1+1]=nums[mid1+1]=nums[mid]
    而当更新 right 的时候有 nums[mid] >= target,也就是有
    n u m s [ r i g h t 1 + 1 ] > = t a r g e t nums[right1+1] >= target nums[right1+1]>=target
    由于每次更新后的 right 都满足这个条件,所以当循环结束时,此时的 right 右边的所有元素都会 大于等于 target, 这就是灵神说的循环不变量。

  5. 同样,对于更新后的 left1
    n u m s [ l e f t 1 − 1 ] = n u m s [ m i d − 1 + 1 ] = n u m s [ m i d ] nums[left1-1] = nums[mid-1+1] = nums[mid] nums[left11]=nums[mid1+1]=nums[mid]
    而当更新 left 的时候有 nums[mid] < target,也就是有
    n u m s [ l e f t 1 − 1 ] < t a r g e t nums[left1-1] < target nums[left11]<target
    所以当循环结束时,此时的 left 左边的所有元素都会 小于 target

  6. 所以循环结束时 left = right+1,在 right 的右边,满足大于等于target,而所有 left 左边的元素都会小于 target,因此 left 就是数组中第一个大于等于 target 元素的下标。

其他写法(半闭半开区间,开区间)

可以阅读 灵茶山艾府 的题解【视频讲解】二分查找总是写不对?三种写法,一个视频讲透!(Python/Java/C++/C/Go/JS)

C++内置函数

//返回第一个大于等于target的迭代器
auto it = lower_bound(nums.begin(), nums.end(), target);
//返回第一个严格大于target的迭代器
auto it = upper_bound(nums.begin(), nums.end(), target); 
//返回it迭代器的前一个元素
prev(it); 

自定义比较函数举例

//使用 lambda 表达式
auto cmp = [](const int x, const int y)->bool{ return x<y; };
auto it = lower_bound(nums.begin(), nums.end(), target, cmp);

//使用函数体
bool cmp(const int x, const int y){ 
	return x<y;
}
auto it = lower_bound(nums.begin(), nums.end(), target, cmp);

提醒:

  1. 使用 lower_boundupper_bound 函数之前需要确保容器中的元素有序
  2. 当数组为 时,此时 it = nums.begin() = nums.end(), 对 it 本身或者前一个元素和后一个元素的访问会非法。
  3. 当不存在满足条件的下标时,it = nums.end()
  4. 自定义比较函数如果使用函数体书写且写在类的内部,要加上 static 关键字,因为成员函数(非静态)隐含地接受一个指向其所属类实例的this指针作为第一个参数。因此,如果尝试将成员函数作为比较函数传递给算法时,算法的参数与成员函数的期望参数不匹配。

二分查找的几种情况转换

  1. 查找第一个大于等于 target 的元素的下标
auto it =  lower_bound(nums.begin(), nums.end(), target);
int index = it - nums.begin();
  1. 查找第一个大于 target 的元素的下标
auto it =  upper_bound(nums.begin(), nums.end(), target);
int index = it - nums.begin();
  1. 查找最后一个小于等于 target 的元素的下标
//等价于查找第一个大于 target 元素的前一个元素
auto it =  upper_bound(nums.begin(), nums.end(), target);
int index = it - nums.begin() - 1;
  1. 查找最后一个小于 target 的元素的下标
//等价于查找第一个大于等于 target 元素的前一个元素
auto it =  lower_bound(nums.begin(), nums.end(), target);
int index = it - nums.begin() - 1;

二分查找有关题目的思路

由于和二分查找有关的题目可以使用内置函数来解决查找的问题,所以解决这一类题目的关键就在于:

  1. 判断数组中是否存在有序的部分。
  2. 判断有序部分是否能利用,以及如何利用。
  3. 理清答案和返回的各个迭代器之间的关系

标准二分查找

34. 在排序数组中查找元素的第一个和最后一个位置

题目描述:
  给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
  如果数组中不存在目标值 target,返回 [-1, -1]。
  你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

代码:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
       auto start = lower_bound(nums.begin(), nums.end(), target);
       auto end = upper_bound(nums.begin(), nums.end(), target);
       if(start == nums.end() || *start!=target){
            return {-1,-1};
       }
       int startIndex = start - nums.begin();
       int endIndex = end - nums.begin() - 1;
       return {startIndex, endIndex};
    }
};

35. 搜索插入位置

题目描述:
  给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
  请必须使用时间复杂度为 O(log n) 的算法。

代码:

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int index = lower_bound(nums.begin(),nums.end(),target)-nums.begin();
        return index;
    }
};

两个数组,一个数组遍历,一个数组二分查找

  当出现两个数组 arr1, arr2时,且当 arr1[i] 固定时,满足答案的 arr2[j] 与一个区间 Q有关,判断这个区间 Q 的边界是否能由二分查找得到,以降低复杂度。

1385. 两个数组间的距离值

题目描述:
  给你两个整数数组 arr1arr2 和一个整数 d ,请你返回两个数组之间的 距离值
  「距离值」 定义为符合此距离要求的元素数目:对于元素 arr1[i] ,不存在任何元素 arr2[j] 满足 |arr1[i]-arr2[j]| <= d 。

思路:
  当 x = arr[i] 固定,对 arr2 的区间 Q = [x-d, x+d], 当 arr2 在Q中的元素为 0 时 ,满足题目要求。
  求 [x,y] 中元素的个数可以用 (-INF, y] 中元素的个数减去 (-INF,x) 中元素的个数。

代码:

class Solution {
public:
    int findTheDistanceValue(vector<int>& arr1, vector<int>& arr2, int d) {
       ranges::sort(arr2);
       int ans = 0;
       for(auto &x:arr1){
            int num1 = upper_bound(arr2.begin(), arr2.end(), x+d) - arr2.begin() - 1;
            int num2 = lower_bound(arr2.begin(), arr2.end(), x-d) - arr2.begin() - 1;
            if(num1-num2==0) ans++;
       }
       return ans;
    }
};

  由于这道题 [x-d,x+d] 中的元素个数为 0 就满足要求,因此只需判断是否有至少一个元素在区间 [x-d,x+d] 中。
  求第一个大于等于 x-d 的值,如果存在且这个值小于等于 x+d,则至少一个元素(即这个元素)在区间中。
  如果不存在第一个大于等于 x-d 的值,则 arr2 中的所有元素都小于 x-d ;如果存在且这个值大于 x+d ,那么在这个值之前的值都小于 x-d, 这个值及这个值之后 的值都大于 x+d

代码:

class Solution {
public:
    int findTheDistanceValue(vector<int>& arr1, vector<int>& arr2, int d) {
       ranges::sort(arr2);
       int ans = 0;
       for(auto &x:arr1){
            auto it = lower_bound(arr2.begin(), arr2.end(), x-d);
            if(it==arr2.end() || *it>d+x) ans++;
       }
       return ans;
    }
};

2300. 咒语和药水的成功对数

题目描述:
  给你两个正整数数组 spellspotions ,长度分别为 n 和 m ,其中 spells[i] 表示第 i 个咒语的能量强度,potions[j] 表示第 j 瓶药水的能量强度。
  同时给你一个整数 success 。一个咒语和药水的能量强度 相乘 如果 大于等于 success ,那么它们视为一对 成功 的组合。
  请你返回一个长度为 n 的整数数组 pairs,其中 pairs[i] 是能跟第 i 个咒语成功组合的 药水 数目。

思路:
  当 spells[i] 固定时,所有满足条件的 potions[j] 满足在区间 [success/spells[i], +INF) 的区间上,统计该区间中元素的个数即可。

代码:

class Solution {
public:
    vector<int> successfulPairs(vector<int>& spells, vector<int>& potions, long long success) {
        ranges::sort(potions);
        vector<int> pairs;
        for(auto &spell:spells){
            long long minPotion = (success + static_cast<long long>(spell) - 1)/spell;  
            auto iter = lower_bound(potions.begin(), potions.end(), minPotion);
            pairs.emplace_back(potions.end()-iter);
        }
        return pairs;
    }
};

有关数组中数对的数目

  由于选择一个数对和这个数对的顺序无关,故可以考虑将数组进行排序,在此基础上一个数固定,另一个数通过二分查找来降低复杂度。

统计公平数对的数目

题目描述:
  给你一个下标从 0 开始、长度为 n 的整数数组 nums ,和两个整数 lower 和 upper ,返回 公平数对的数目 。
  如果 (i, j) 数对满足以下情况,则认为它是一个 公平数对
    0 <= i < j < n,且
    lower <= nums[i] + nums[j] <= upper

代码:

class Solution {
public:
    long long countFairPairs(vector<int>& nums, int lower, int upper) {
        ranges::sort(nums);
        long long ans = 0;
        int n = nums.size();
        for(int i=0;i<n;++i){
            auto left = lower_bound(nums.begin()+i+1, nums.end(), lower-nums[i]);
            auto right = upper_bound(nums.begin()+i+1, nums.end(), upper-nums[i]);
            ans += right - left;
        }
        return ans;
    }
};

设计数据结构(注意到题目潜在描述中的有序信息)

  1. 思考怎样利用有序性构造数据结构,从而使用二分法来查找答案。C++内置二分查找的函数返回的是数组迭代器,要确定答案和所构造的数据结构的下标有关,还是和迭代器对应的数据有关。
  2. 注意到一些会严格自增的变量,比如时间等。

2080. 区间内查询数字的频率

题目描述:
  请你设计一个数据结构,它能求出给定子数组内一个给定值的频率
  子数组中一个值的 频率 指的是这个子数组中这个值的出现次数。
请你实现 RangeFreqQuery 类:
  RangeFreqQuery(int[] arr) 用下标从 0 开始的整数数组 arr 构造一个类的实例。
  int query(int left, int right, int value) 返回子数组 arr[left…right] 中 value 的 频率 。
  一个 子数组 指的是数组中一段连续的元素。arr[left…right] 指的是 nums 中包含下标 leftright 在内 的中间一段连续元素。

思路:
  在 [0, right] 的子数组中,一个给定值 value 的频率满足这样的规律,right 越大,value的频率 不变或更大,这满足有序性。但是根据这种 有序性 来查找,是根据 value的频率 来查找对应的下标关系,这和题目要求关系不大。
  反过来思考,这种有序性还可表示为 value的频率 越大,right 就越大,这样查找就是根据下标来找 value的频率。这就要求对于一个给定值 value, 要有一个以递增的 value的频率 为下标,以数组原本下标为值的数据结构用于二分查找。
  因此我们想到将 value的下标 存为一个数组,命名为 vec。那么有 [vec[i], vec[i+1])value的频率等于 i+1[left, right]value 的频率为 [0,right]value 的频率 减去 [0,left)value 的频率。前者等于第一个大于right的值的前一个值的下标+1,后者等于第一个大于等于left的值的前一个值的下标+1,这就可以用二分法求解了,代码是将这个减法化简的结果。

代码:

class RangeFreqQuery {
public:
    unordered_map<int,vector<int>> mp;
    RangeFreqQuery(vector<int>& arr) {
        for(int i=0;i<arr.size();++i){
            mp[arr[i]].emplace_back(i);
        }
    }
    
    int query(int left, int right, int value) {
        auto rightN = upper_bound(mp[value].begin(), mp[value].end(), right);
        auto leftN = lower_bound(mp[value].begin(), mp[value].end(), left);
        return rightN - leftN;
    }
};

981. 基于时间的键值存储

题目描述:
  设计一个基于时间的键值数据结构,该结构可以在不同时间戳存储对应同一个键的多个值,并针对特定时间戳检索键对应的值。
  实现 TimeMap 类
  TimeMap() 初始化数据结构对象
  void set(String key, String value, int timestamp) 存储给定时间戳 timestamp 时的键 key 和值 value。
  String get(String key, int timestamp) 返回一个值,该值在之前调用了 set,其中 timestamp_prev <= timestamp 。如果有多个这样的值,它将返回与最大 timestamp_prev 关联的值。如果没有值,则返回空字符串(“”)。

思路
  需要注意到的一定是这是一个基于时间的数据结构,set 操作中的时间戳都是严格递增的,满足有序性,其他方面和上题类似。

代码:

class TimeMap {
public:
    unordered_map<string, vector<pair<int, string>>> mp;
    TimeMap() {
        mp = unordered_map<string, vector<pair<int, string>>>();
    }
    
    void set(string key, string value, int timestamp) {
        mp[key].emplace_back(timestamp, value);
    }
    
    string get(string key, int timestamp) {
        auto &tmp = mp[key];
        auto it = lower_bound(tmp.begin(), tmp.end(), make_pair(timestamp,""));
        if(it!=tmp.end() && it->first == timestamp){
            return it->second;
        }
        else{
            if(it==tmp.begin()) return "";
            return (--it)->second;
        }
    }
};

1146. 快照数组

题目描述:
  实现支持下列接口的「快照数组」- SnapshotArray
  SnapshotArray(int length) - 初始化一个与指定长度相等的 类数组 的数据结构。初始时,每个元素都等于 0。
  void set(index, val) - 会将指定索引 index 处的元素设置为 val。
  int snap() - 获取该数组的快照,并返回快照的编号 snap_id(快照号是调用 snap() 的总次数减去 1)。
  int get(index, snap_id) - 根据指定的 snap_id 选择快照,并返回该快照指定索引 index 的值。

思路:
  注意到快照号的递增性。

代码:

class SnapshotArray {
public:
    int curSnapId = 0;
    vector<vector<pair<int,int>>> snapVec;
    SnapshotArray(int length) {
        snapVec = vector<vector<pair<int,int>>>(length);
    }
    
    void set(int index, int val) {
        snapVec[index].emplace_back(curSnapId,val);
    }
    
    int snap() {
        return curSnapId++;
    }
    
    int get(int index, int snap_id) {
        auto &vec = snapVec[index];
        auto it = upper_bound(vec.begin(), vec.end(), make_pair(snap_id+1, -1));
        return it==vec.begin()?0:prev(it)->second;
    }
};

二分答案

  当一个变量 x 和给定的限制 limit 有单调性的关系时,我们可以用二分法遍历 x 来找到最后的答案,而不用将 x 全部遍历完,从而降低算法的复杂性。最后返回的题目的答案是满足题目要求的 x 值。

  一般而言,可以这样思考,定义函数 check(x), 表示选择 x 值时是否满足题目要求,其中 x 表示要最大化或者最小化的那个值,那么有 check(x) 有这样的特性:

c h e c k ( x ) = { f a l s e , x < x a n s t r u e , x ≥ x a n s check(x)=\left\{ \begin{aligned} false, &\quad \quad x<x_{ans}\\ true, &\quad \quad x \geq x_{ans} \end{aligned} \right. check(x)={false,true,x<xansxxans
  由于 check(x) 具有单调性,所以我们可以用二分法来求得答案 k a n s k_{ans} kans。二分答案时,选择合适的 leftright 作为初始值可以有效的缩小二分查找的范围。
  定义的 check(x) 一般类似于:

c h e c k ( x ) = { f a l s e , 当取 x 时不满足题目条件 t r u e , 当取 x 时满足题目条件 check(x)=\left\{ \begin{aligned} false, &\quad \quad 当取x时不满足题目条件\\ true, &\quad \quad 当取x时满足题目条件 \end{aligned} \right. check(x)={false,true,当取x时不满足题目条件当取x时满足题目条件
  我们设要最大化或者要最小化的值为 x, 思考当 x 值一定时如何判断是否满足题目要求,以此来写 check 函数和进行二分查找。
  在求最大化最小值和最小化最大值时,最小值或最大值的判断往往要用到贪心策略。
  求第 k 小等价于:求最小的 x, 满足小于等于 x 的值至少有 k 个;求第 k 大等价于:求最大的 x,满足大于等于 x 的值至少有 k 个。

二分答案的模板(闭区间写法)

int n = nums.size();
int left, right; 
while(left<=right){
    int mid = left + (right-left)/2;  //防止整型溢出
    if(check(mid)){  
        right = mid-1;  
    }
    else{
        left = mid+1; 
    }
}
//循环不变量:对于right右边的值r始终有check(r) = true;
//循环不变量:对于left左边的值l始终有check(l) = false;
return 答案;  //答案根据check函数定义的循环不变量和题目要求具体来看   

求最小

2187. 完成旅途的最少时间

题目描述:

  给你一个数组 time ,其中 time[i] 表示第 i 辆公交车完成 一趟旅途 所需要花费的时间。
  每辆公交车可以 连续 完成多趟旅途,也就是说,一辆公交车当前旅途完成后,可以 立马开始 下一趟旅途。每辆公交车 独立 运行,也就是说可以同时有多辆公交车在运行且互不影响。
  给你一个整数 totalTrips ,表示所有公交车 总共 需要完成的旅途数目。请你返回完成 至少 totalTrips 趟旅途需要花费的 最少 时间。

思路:
  1.花费的时间是要最小化的值,把它定义为 x, 用 l i m i t x limit_x limitx 表示时间 x 内所有公交车 总共 需要完成的旅途数目,则有:

l i m i t x = ∑ i = 0 n ⌊ x t i m e [ i ] ⌋ limit_x= \sum_{i=0}^n \lfloor { \frac {x}{time[i]} } \rfloor limitx=i=0ntime[i]x
  定义 check(x) 函数:

c h e c k ( x ) = { f a l s e , l i m i t x < t o t a l T r i p s t r u e , l i m i t x ≥ t o t a l T r i p s check(x) = \left\{ \begin{aligned} false, &\quad \quad limit_x<totalTrips\\ true, &\quad \quad limit_x \geq totalTrips \end{aligned} \right. check(x)={false,true,limitx<totalTripslimitxtotalTrips
  由于 x 越大,limit 越大,check(x) 越有可能为 true,因此可以用二分法求最小 check(x) = true 的 x 值 x a n s x_{ans} xans

  2.如果 check(m) = false, 那说明所有 m 左边的值 m l e f t m_{left} mleft 都不能满足条件。因为 m l e f t < m m_{left} < m mleft<m => l i m i t m l e f t < l i m i t m < t o t a l T r i p s limit_{mleft} < limit_m <totalTrips limitmleft<limitm<totalTrips,所以 c h e c k ( m l e f t ) = f a l s e check(m_{left}) = false check(mleft)=false。因此 x a n s x_{ans} xans 应该在 m 右侧, 将 left 更新为 left = m+1。(闭区间写法)
  同理,如果 check(m) = true,那说明所有 m 右边的值都能满足条件。因此 k a n s k_{ans} kans 可能在 m 左侧, 将 right 更新为 right = m-1。(闭区间写法)
  当循环结束后,left + 1 = right, 所有 right 右边的值都不满足条件, 所有 left 左边的值都满足条件,left 在 right 左边,故 right 是最大的满足条件的值,即所求的答案。

  3.由于1 <= time[i], totalTrips <= 1e7, 那么当 t=1 时, left最小为min(time),保证有一个能完成,此时的 left 为全局可能答案中的最小值。
  right m i n ( t i m e ) ∗ t o t a l T r i p s min(time)*totalTrips min(time)totalTrips ,保证至少有 totalTrips 个完成,此时的 right 为全局可能答案的最大值。
  left, right 的值可以根据情况而定,只要保证所有的答案都在 [left, right]区间内(这是闭区间的写法,闭区间特殊情况也可以不包含,但需要对取那些值的情况进行特殊判定,是否不包含也能得到正确结果)

代码:

class Solution {
public:
    long long minimumTime(vector<int>& time, int totalTrips) {
        long long left = *min_element(time.begin(), time.end());
        long long right = left*totalTrips;

        auto check = [&](long long mid)->bool{
            long long midV = 0;
            for(int i=0;i<time.size();++i){
                midV += mid/time[i];
            }
            return midV>=totalTrips;
        };

        while(left<=right){
            long long mid = left + (right-left)/2;
            if(check(mid)){
                right = mid-1;
            }
            else{
                left = mid+1;
            }
        }
        //left左边的所有l满足check(l) = false,即小于totalTrips;
        //right右边的所有r满足check(r) = true,即大于等于totalTrips;
        //循环结束时left = right+1, 在right右边,则所有在left左边的小于totalTrips,left大于等于totalTrips,即left为下标的数是第一个大于等于totalTrips的数, left即为最终的答案
        return left;
    }
};

1283. 使结果不超过阈值的最小除数

题目描述:
  给你一个整数数组 nums 和一个正整数 threshold ,你需要选择一个正整数作为除数,然后将数组里每个数都除以它,并对除法结果求和。
  请你找出能够使上述结果小于等于阈值 threshold 的除数中 最小 的那个。
  每个数除以除数后都向上取整,比方说 7/3 = 3 , 10/2 = 5 。
  题目保证一定有解。
思路:
   除数为 x 时,和为 l i m i t x limit_{x} limitx,则有:
l i m i t x = ∑ i = 0 n ⌈ n u m s [ i ] x ⌉ = ∑ i = 0 n ⌊ n u m s [ i ] + k − 1 x ⌋ = ∑ i = 0 n ⌊ n u m s [ i ] − 1 x ⌋ + 1 \begin{align*} limit_x&= \sum_{i=0}^n \lceil { \frac {nums[i]}{x} } \rceil \\ &= \sum_{i=0}^n \lfloor { \frac {nums[i]+k-1}{x} } \rfloor \\ &= \sum_{i=0}^n \lfloor { \frac {nums[i]-1}{x} } \rfloor +1 \end{align*} limitx=i=0nxnums[i]=i=0nxnums[i]+k1=i=0nxnums[i]1+1
  定义 check(x) 函数:

c h e c k ( x ) = { t r u e , l i m i t x ≤ t h r e s h o l d f a l s e , l i m i t x > t h r e s h o l d check(x) = \left\{ \begin{aligned} true, &\quad \quad limit_x\leq threshold\\ false, &\quad \quad limit_x > threshold \end{aligned} \right. check(x)={true,false,limitxthresholdlimitx>threshold
  x 越大, l i m i t x limit_{x} limitx 越小,check(x) 越有可能为 true,可以用二分答案来求。
代码:

class Solution {
public:
    int smallestDivisor(vector<int>& nums, int threshold) {
        int n = nums.size();
        int left = 1, right = *max_element(nums.begin(), nums.end());
        
        auto check = [&](int mid)->bool{
            int midV = 0;
            for(int i=0;i<n;++i){
                midV += (nums[i]-1)/mid + 1;
            }
            return midV<=threshold;    
        };
        
        while(left<=right){
            int mid = left + (right-left)/2;
            if(check(mid)){
                right = mid-1;
            }
            else{
                left = mid+1;
            }
        }
        return left;
    }
};

求最大

275. H 指数 II

题目描述:
  给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数,citations 已经按照 升序排列 。计算并返回该研究者的 h 指数。
  h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)至少 有 h 篇论文分别被引用了至少 h 次。
  请你设计并实现对数时间复杂度的算法解决此问题。

思路:
  “至少 有 h 篇论文分别被引用了至少 h 次” 就是求 “满足x篇论文至少分别被引用了x次” 的所有 x 值的 最大值
  定义 check(x) 函数:

c h e c k ( x ) = { t r u e , x 篇论文至少分别引用了 x 次 f a l s e , e l s e . check(x) = \left\{ \begin{aligned} true, &\quad \quad x篇论文至少分别引用了x次\\ false, &\quad \quad else. \end{aligned} \right. check(x)={true,false,x篇论文至少分别引用了xelse.
  发现当 x 越大时,check(x) 越有可能为 false。如果 check(x) = false, check(x+1) = false; 如果 check(x) = true, check(x-1) = true。满足单调性,可以用二分法来求 x a n s x_{ans} xans

代码:

class Solution {
public:
    int hIndex(vector<int>& citations) {
        int n = citations.size();
        int left = 1, right = n;  
        while(left<=right){
            int mid = left + (right-left)/2;
            if(citations[n-mid]>=mid){
                left = mid+1;
            }
            else{
                right = mid-1;
            }
        }
        return right;
    }
};

提醒:
  答案范围是 [0,n], 但是闭区间范围初始范围是 [1,n], 防止 n-mid 时为n越界。
  因此要特判 ans为 0 时的情况。当且仅当 citations.back() = 0, ans 为 0。 此时循环内只会移动 right, 循环结束后 left=1,right = left - 1 = 0,答案正确。

2226. 每个小孩最多能分到多少糖果

题目描述:
  给你一个 下标从 0 开始 的整数数组 candies 。数组中的每个元素表示大小为 candies[i] 的一堆糖果。你可以将每堆糖果分成任意数量的 子堆 ,但 无法 再将两堆合并到一起。
  另给你一个整数 k 。你需要将这些糖果分配给 k 个小孩,使每个小孩分到 相同 数量的糖果。每个小孩可以拿走 至多一堆 糖果,有些糖果可能会不被分配。
  返回每个小孩可以拿走的 最大糖果数目

思路:
  定义要最大化的值即糖果数目为 x, 定义 check(x) 表示糖果数目为 x 时是否能够分给所有的孩子:

c h e c k ( k ) = { t r u e , 糖果数目为 x 时能够分给所有的孩子 f a l s e , e l s e . check(k) = \left\{ \begin{aligned} true, &\quad \quad 糖果数目为 x 时能够分给所有的孩子\\ false, &\quad \quad else. \end{aligned} \right. check(k)={true,false,糖果数目为x时能够分给所有的孩子else.
  可以证明,check(x) 满足单调性,因此可以使用二分法求答案。
代码:

class Solution {
public:
    int maximumCandies(vector<int>& candies, long long k) {
        int n = candies.size();
        int left = max(1LL, (*min_element(candies.begin(), candies.end()))/((k-1)/n+1));
        int right = accumulate(candies.begin(), candies.end(), 0LL)/k;
        
        auto check = [&](int mid)->bool{
            long long midV = 0;
            for(int i=0;i<n;++i){
                midV += candies[i]/mid;
                if(midV>=k) return true;
            }
            return false;
        };
        
        while(left<=right){
            int mid = left + (right-left)/2;
            if(check(mid)){
                left = mid+1;
            }
            else{
                right = mid-1;
            }
        }
        return right;
    }
};

二分间接值

3143. 正方形中的最多点数

1648. 销售价值减少的颜色球

最小化最大值

410. 分割数组的最大值

题目描述:
  给定一个非负整数数组 nums 和一个整数 k ,你需要将这个数组分成 k 个非空的连续子数组,使得这 k 个子数组各自和的最大值 最小
  返回分割后最小的和的最大值。
  子数组 是数组中连续的部份。

思路:
  定义要最小化的值为 x,即分割后这 k 个子数组各自和的最大值。
  定义 check(x) 表示当分割后的子数组各自和的最大值小于等于 x 时能否分为 k 个子数组。这样定义的 check(x) 具有单调性,可以用二分法求解。
  定义 “小于等于” 的原因在于二分中的 mid 不一定是子数组的和,定义为 “小于等于” 可以使二分法趋于最小的满足条件的值,就像 lower_bound 返回的是第一个大于等于 target 的值一样。而这个最小的满足条件的值一定是一个子数组的和。因为如果不是,那么这一个值的前一个子数组和一定也满足题目条件,这样的情况下循环并不会停止,因此循环停止时得到的最后结果一个是一个子数组的和。
  如何判断分割后的子数组各自和的最大值小于等于 x 时能否分为 k 个子数组需要用到贪心的策略:每分割一个数组,尽可能多的向其中添加元素,使其各元素和小于等于x。这样能够分得的子数组个数 num 最小,当 num ≤ \leq k 时,说明能够被分为 k 个子数组(num < k 可以从前面的数组中任取若干元素,每个元素单独为一个数组,也能满足 num = k 这个要求)。

代码:

class Solution {
public:
    int splitArray(vector<int>& nums, int k) {
        int n = nums.size();
        int left = *max_element(nums.begin(), nums.end());
        int right = accumulate(nums.begin(), nums.end(), 0);

        auto check = [&](int mid)->bool{
            int midV = 1;
            int temp = 0;
            for(int i=0;i<n;++i){
                if(temp+nums[i]<=mid){
                    temp += nums[i];
                }
                else{
                    temp = nums[i];
                    midV++;
                    if(midV>k) return true;
                }
            }
            return false;
        };

        while(left<=right){
            int mid = left + (right-left)/2;
            if(check(mid)){
                left = mid+1;
            }
            else{
                right = mid-1;
            }
        }
        return left;
    }
};

2064. 分配给商店的最多商品的最小值

题目描述:
  给你一个整数 n ,表示有 n 间零售商店。总共有 m 种产品,每种产品的数目用一个下标从 0 开始的整数数组 quantities 表示,其中 quantities[i] 表示第 i 种商品的数目。
  你需要将 所有商品 分配到零售商店,并遵守这些规则:
    一间商店 至多 只能有 一种商品 ,但一间商店拥有的商品数目可以为 任意 件。
    分配后,每间商店都会被分配一定数目的商品(可能为 0 件)。用 x 表示所有商店中分配商品数目的最大值,你希望 x 越小越好。也就是说,你想 最小化 分配给任意商店商品数目的 最大值
  请你返回最小的可能的 x 。

思路:
  定义要最小化的值为 x, 即 x 表示分配给任意商店商品数目的最大值。定义 check(x) 表示当分配给任意商店商品数目的值都小于等于x时,所有商品能不能被分完,check(x) 具有单调性,使用贪心策略判断所有商品能不能分完。

代码:

class Solution {
public:
    int minimizedMaximum(int n, vector<int>& quantities) {
        int left = max(1LL, accumulate(quantities.begin(), quantities.end(), 0LL)/n);
        int right = *max_element(quantities.begin(), quantities.end());
        
        auto check = [&](int mid)->bool{
            int midV = 0;
            for(auto &it:quantities){
                midV += (it-1)/mid + 1;
                if(midV>n) return false;
            }
            return true;
        };
        
        while(left<=right){
            int mid = left + (right-left)/2;
            if(check(mid)){
                right = mid - 1;
            }
            else{
                left = mid + 1;
            }
        }
        return left;
    }
};

最大化最小值

3281. 范围内整数的最大得分

题目描述:
  给你一个整数数组 start 和一个整数 d,代表 n 个区间 [start[i], start[i] + d]。
  你需要选择 n 个整数,其中第 i 个整数必须属于第 i 个区间。所选整数的 得分 定义为所选整数两两之间的 最小 绝对差。
  返回所选整数的 最大可能得分

思路:
  定义要最大化的值为 x, 即 x 表示所选整数两两之间的 最小 绝对差。定义 check(x) 表示是否存在这样的选择,使所选整数两两之间的绝对差都大于等于 x 。

代码:

class Solution {
public:
    int maxPossibleScore(vector<int>& start, int d) {
        sort(start.begin(), start.end());
        int n = start.size();
        int left = 0;
        int right = (start.back() + d - start[0] - 1)/(n-1) + 1;

        auto check = [&](int mid)->bool{
            int temp = start[0];
            for(int i=1;i<n;++i){
                if(temp<=start[i]+d-mid){
                    temp = max(temp+mid,start[i]);
                }
                else return false;
            }
            return true;
        };

        while(left<=right){
            int mid = left + (right-left)/2;
            if(check(mid)){
                left = mid + 1;
            }
            else{
                right = mid - 1;
            }
        }
        return right;
    }
};

第k小/大

668. 乘法表中第k小的数

题目描述:
  几乎每一个人都用 乘法表。但是你能在乘法表中快速找到第 k 小的数字吗?
  乘法表是大小为 m x n 的一个整数矩阵,其中 mat[i][j] == i * j(下标从 1 开始)。
  给你三个整数 m、n 和 k,请你在大小为 m x n 的乘法表中,找出并返回第 k 小的数字。

代码:

class Solution {
public:
    int findKthNumber(int m, int n, int k) {
        int left = 1;
        int right = m*n;

        auto check = [&](int mid)->bool{
            int midV = 0;
            for(int i=1;i<=m;++i){
                int tempV = min(n,mid/i);
                if(tempV==0) break;
                midV += tempV;
                if(midV>=k) return true;
            }
            return false;
        };

        while(left<=right){
            int mid = left + (right-left)/2;
            if(check(mid)){
                right = mid-1;
            }
            else{
                left = mid+1;
            }
        }
        return left;
    }
};

二分查找的变形

  当数据并不是全局有序,而是局部有序的时候,此时使用二分查找就要格外注意在循环中移动 l e f t left left r i g h t right right 的条件。
  此时思考的关键在于找到划分区间的方式,使得其中一个区间一定不会包含目标元素。难点在于思考如何划分区间,和怎么判断这个区间不会包含目标元素。

旋转排序数组

33. 搜索旋转排序数组

题目描述:
  整数数组 n u m s nums nums 按升序排列,数组中的值 互不相同
  在传递给函数之前, n u m s nums nums 在预先未知的某个下标 k ( 0 < = k < n u m s . l e n g t h k(0 <= k < nums.length k0<=k<nums.length 上进行了 旋转,使数组变为 [ n u m s [ k ] , n u m s [ k + 1 ] , . . . , n u m s [ n − 1 ] , n u m s [ 0 ] , n u m s [ 1 ] , . . . , n u m s [ k − 1 ] ] [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]] [nums[k],nums[k+1],...,nums[n1],nums[0],nums[1],...,nums[k1]](下标 从 0 开始 计数)。例如, [ 0 , 1 , 2 , 4 , 5 , 6 , 7 ] [0,1,2,4,5,6,7] [0,1,2,4,5,6,7] 在下标 3 3 3 处经旋转后可能变为 [ 4 , 5 , 6 , 7 , 0 , 1 , 2 ] [4,5,6,7,0,1,2] [4,5,6,7,0,1,2]
  给你 旋转后 的数组 n u m s nums nums 和一个整数 t a r g e t target target ,如果 n u m s nums nums 中存在这个目标值 t a r g e t target target ,则返回它的下标,否则返回 − 1 -1 1
  你必须设计一个时间复杂度为 O ( l o g n ) O(log n) O(logn) 的算法解决此问题。

思考:
  对于数组中的任何一个值,一定能够将数组划分为左右两个区间,其中一个区间是 有序的,那么我们就可以通过判断 t a r g e t target target 是否在这一个有序的区间中来每次舍弃一个区间:如果 t a r g e t target target 在有序的区间中,那么舍弃掉另一个区间;否则,舍弃掉有序的区间。

代码:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0, right = n-1;
        while(left<=right){
            int mid = left + (right-left)/2;
            if(nums[mid]==target) return mid;
            if(nums[mid]>=nums[left]){ //[left,mid]是有序的
                if(target>=nums[left] && target<nums[mid]){   //target不在[left,mid]中
                    right = mid-1; //之后在[left,mid]中查找
                }
                else{
                    left = mid+1;
                }
            }
            else{ //[mid, right]是有序的
                if(target>nums[mid] && target<=nums[right]){ //target在[mid,right]中
                    left = mid+1; //之后在[mid,right]中查找
                }
                else{
                    right = mid-1;
                }
            }
        }
        return -1;
    }
};

81. 搜索旋转排序数组 II

题目描述:
  已知存在一个按非降序排列的整数数组 n u m s nums nums ,数组中的值不必互不相同。
  在传递给函数之前, n u m s nums nums 在预先未知的某个下标 k ( 0 < = k < n u m s . l e n g t h ) k(0 <= k < nums.length) k0<=k<nums.length上进行了 旋转 ,使数组变为 [ n u m s [ k ] , n u m s [ k + 1 ] , . . . , n u m s [ n − 1 ] , n u m s [ 0 ] , n u m s [ 1 ] , . . . , n u m s [ k − 1 ] ] [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]] [nums[k],nums[k+1],...,nums[n1],nums[0],nums[1],...,nums[k1]](下标 从 0 开始 计数)。例如, [ 0 , 1 , 2 , 4 , 4 , 4 , 5 , 6 , 6 , 7 ] [0,1,2,4,4,4,5,6,6,7] [0,1,2,4,4,4,5,6,6,7] 在下标 5 5 5 处经旋转后可能变为 [ 4 , 5 , 6 , 6 , 7 , 0 , 1 , 2 , 4 , 4 ] [4,5,6,6,7,0,1,2,4,4] [4,5,6,6,7,0,1,2,4,4]
  给你 旋转后 的数组 n u m s nums nums 和一个整数 t a r g e t target target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 n u m s nums nums 中存在这个目标值 t a r g e t target target ,则返回 t r u e true true ,否则返回 f a l s e false false
  你必须尽可能减少整个操作步骤。

思考:
  这一题和上一题的区别在于有重复元素,那么对于一个元素 n u s m [ m i d ] nusm[mid] nusm[mid] ,当它将数组分为两个区间时,当 n u m s [ l e f t ] = n u m s [ r i g h t ] = n u m s [ m i d ] nums[left] = nums[right] = nums[mid] nums[left]=nums[right]=nums[mid] 时,我们并不能判断 t a r g e t target target 究竟在哪个区间,所以可以选择将 r i g h t right right 左移, l e f t left left 右移 ( 当 n u m s [ m i d ] = t a r g e t nums[mid]=target nums[mid]=target 时直接返回结果 )。这样就可以不断地缩小区间了,但是最坏情况下的时间复杂度是 O ( n ) O(n) O(n)

代码:

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0, right = n-1;
        while(left<=right){
            int mid = left + (right-left)/2;
            if(nums[mid]==target) return true;
            if(nums[mid]==nums[left] && nums[mid]==nums[right]){
                left++;
                right--;
            }
            else if(nums[mid]>=nums[left]){ //[left, mid]是有序的一个区间
                if(target>=nums[left] && target<nums[mid]){
                    right = mid-1;
                }
                else{
                    left = mid+1;
                }
            }   
            else{ //[mid, right]是一个有序的区间
                if(target>nums[mid] && target<=nums[n-1]){
                    left = mid+1;
                }
                else{
                    right = mid-1;
                }
            }
        }
        return false;
    }
};

153. 寻找旋转排序数组中的最小值

题目描述:
  已知一个长度为 n n n 的数组,预先按照升序排列,经由 1 1 1 n n n 次 旋转 后,得到输入数组。例如,原数组 n u m s = [ 0 , 1 , 2 , 4 , 5 , 6 , 7 ] nums = [0,1,2,4,5,6,7] nums=[0,1,2,4,5,6,7] 在变化后可能得到:
  若旋转 4 4 4 次,则可以得到 [ 4 , 5 , 6 , 7 , 0 , 1 , 2 ] [4,5,6,7,0,1,2] [4,5,6,7,0,1,2]
  若旋转 7 7 7 次,则可以得到 [ 0 , 1 , 2 , 4 , 5 , 6 , 7 ] [0,1,2,4,5,6,7] [0,1,2,4,5,6,7]
  注意,数组 [ a [ 0 ] , a [ 1 ] , a [ 2 ] , . . . , a [ n − 1 ] ] [a[0], a[1], a[2], ..., a[n-1]] [a[0],a[1],a[2],...,a[n1]] 旋转一次 的结果为数组 [ a [ n − 1 ] , a [ 0 ] , a [ 1 ] , a [ 2 ] , . . . , a [ n − 2 ] ] [a[n-1], a[0], a[1], a[2], ..., a[n-2]] [a[n1],a[0],a[1],a[2],...,a[n2]]
  给你一个元素值 互不相同 的数组 n u m s nums nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
  你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

思路:
  仍然是旋转排序数组,当 n u m s [ m i d ] nums[mid] nums[mid] 将数组划分为两个区间时,我们看一看能否判断出 最小值 不在其中一个区间内。
  如果此时 n u m s [ m i d ] nums[mid] nums[mid] 小于等于 n u m s [ r i g h t ] nums[right] nums[right],那么最小值一定是在 [ l e f t , m i d − 1 ] [left,mid-1] [left,mid1] 内 (闭区间写法需要判断 n u m s [ m i d ] nums[mid] nums[mid] 是不是最小值,如果不判断,会漏过可能的最小值,由于整个数组不是全局有序的,循环结束后的不变量不一定是全局的);否则最小值一定在 n u m s [ l e f t ] nums[left] nums[left] 内。

代码:

class Solution {
public:
    int findMin(vector<int>& nums) {
        int n = nums.size();
        int left = 0, right = n-1;
        while(left<=right){
            int mid = left + (right-left)/2;
            if(mid>0 && mid<n-1 && nums[mid]<nums[mid+1] && nums[mid]<nums[mid-1]){
                return nums[mid];
            }
            if(nums[mid]>nums[right]){
                left = mid+1;
            }
            else{
                right = mid-1;
            }
        }
        return nums[left];
    }
};

山脉数组,山脉矩阵

  “人往高处走”,每一次都走比当前位置大的元素,直到不能走为止,此时的元素就是一个峰值。

852. 山脉数组的峰顶索引

题目描述:
  给定一个长度为 n n n 的整数 山脉 数组 a r r arr arr ,其中的值递增到一个 峰值元素 然后递减。
  返回峰值元素的下标。
  你必须设计并实现时间复杂度为 O ( l o g ( n ) ) O(log(n)) O(log(n)) 的解决方案。

思路:
  如果 n u m s [ m i d + 1 ] > n u m s [ m i d ] nums[mid+1] > nums[mid] nums[mid+1]>nums[mid],那么往 m i d + 1 mid+1 mid+1 走一定会有一个峰值;如果 n u m s [ m i d − 1 ] > n u m s [ m i d ] nums[mid -1] > nums[mid] nums[mid1]>nums[mid],那么往 m i d − 1 mid-1 mid1 走一定会有一个峰值; 否则 m i d mid mid 就是一个峰值。这样每次都可以舍弃另一边的元素。

代码:

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int n = arr.size();
        int left = 0, right = n-1;

		//辅助函数,避免比较时数组越界
        auto check = [&](int x, int y)->bool{
            if(y==-1 || y==n) return true;
            if(x==-1 || x==n) return false;
            return arr[x]>arr[y];
        };

        while(left<=right){
            int mid = left + (right-left)/2;
            if(check(mid, mid-1) && check(mid, mid+1)){
                return mid;
            }
            if(check(mid+1, mid)){
                left = mid + 1;
            }
            else{
                right = mid - 1;
            }
        }
        return 0;
    }
};

1095. 山脉数组中查找目标值

题目描述:
  (这是一个 交互式问题 )
  你可以将一个数组 a r r arr arr 称为 山脉数组 当且仅当:
     a r r . l e n g t h > = 3 arr.length >= 3 arr.length>=3
    存在一些 0 < i < a r r . l e n g t h − 1 0 < i < arr.length - 1 0<i<arr.length1 i i i 使得:
     a r r [ 0 ] < a r r [ 1 ] < . . . < a r r [ i − 1 ] < a r r [ i ] arr[0] < arr[1] < ... < arr[i - 1] < arr[i] arr[0]<arr[1]<...<arr[i1]<arr[i]
     a r r [ i ] > a r r [ i + 1 ] > . . . > a r r [ a r r . l e n g t h − 1 ] arr[i] > arr[i + 1] > ... > arr[arr.length - 1] arr[i]>arr[i+1]>...>arr[arr.length1]
  给定一个山脉数组 m o u n t a i n A r r mountainArr mountainArr ,返回 最小 i n d e x index index 使得    m o u n t a i n A r r . g e t ( i n d e x ) = = t a r g e t mountainArr.get(index) == target mountainArr.get(index)==target。如果不存在这样的 i n d e x index index,返回 -1 。
  你无法直接访问山脉数组。你只能使用 M o u n t a i n A r r a y MountainArray MountainArray 接口来访问数组:
     M o u n t a i n A r r a y . g e t ( k ) MountainArray.get(k) MountainArray.get(k) 返回数组中下标为 k k k 的元素(从 0 0 0 开始)。
     M o u n t a i n A r r a y . l e n g t h ( ) MountainArray.length() MountainArray.length() 返回数组的长度。
  调用 M o u n t a i n A r r a y . g e t MountainArray.get MountainArray.get 超过 100 100 100 次的提交会被判定为错误答案。此外,任何试图绕过在线评测的解决方案都将导致取消资格。

思路:
  由于无法判断 t a r g e t target target 究竟是在 峰值元素 的左边还是右边,所以我们可以先找到 峰值元素 的位置,再在左右两个区间分别使用 二分查找

代码:

class Solution {
public:
    // 二分查找
    int binarySearch(MountainArray &mountainArr, int target, int left, int right, bool increasing) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            int temp = mountainArr.get(mid);
            if (temp == target) return mid;

            if (increasing) {
                if (temp < target) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            } else {
                if (temp > target) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }
        return -1;
    }

    int findInMountainArray(int target, MountainArray &mountainArr) {
        int n = mountainArr.length();
        int left = 0, right = n - 1;

        // 找到山脉的峰值
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (mountainArr.get(mid) < mountainArr.get(mid + 1)) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }

        int peak = left; // 峰值的索引

        // 在上升部分进行二分查找
        int res = binarySearch(mountainArr, target, 0, peak, true);
        if (res != -1) return res;

        // 在下降部分进行二分查找
        return binarySearch(mountainArr, target, peak + 1, n - 1, false);
    }
};

162. 寻找峰值

题目描述:
  峰值元素是指其值严格大于左右相邻值的元素。
  给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
  你可以假设 nums[-1] = nums[n] = -∞ 。
  你必须实现时间复杂度为 O(log n) 的算法来解决此问题。

代码:

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int n = nums.size();
        int left = 0, right = n-1;
        
        auto get = [&](int mid)->pair<int,int>{
            if(mid==-1 || mid==n){
                return {0,0};
            }
            return {1, nums[mid]};
        };

        while(left<=right){
            int mid = left + (right-left)/2;
            if(get(mid)>get(mid+1) && get(mid)>get(mid-1)) return mid;
            if(get(mid+1)>get(mid)){
                left = mid+1;
            }
            else{
                right = mid-1;
            }
        }
        return left;
    }
};

1901. 寻找峰值 II

题目描述:
  一个 2 D 2D 2D 网格中的 峰值 是指那些 严格大于 其相邻格子(上、下、左、右)的元素。
  给你一个 从 0 0 0 开始编号 的 m x n m x n mxn 矩阵 m a t mat mat ,其中任意两个相邻格子的值都 不相同 。找出 任意一个 峰值 m a t [ i ] [ j ] mat[i][j] mat[i][j]返回其位置 [i,j] 。
  你可以假设整个矩阵周边环绕着一圈值为 − 1 -1 1 的格子。
  要求必须写出时间复杂度为 O ( m l o g ( n ) ) O(m log(n)) O(mlog(n)) O ( n l o g ( m ) ) O(n log(m)) O(nlog(m)) 的算法

代码:

class Solution {
public:
    vector<int> findPeakGrid(vector<vector<int>>& mat) {
        int m = mat.size(), n = mat[0].size();
        int up = 0, down = m-1;
        while(up<=down){
            int mid = up + (down-up)/2;
            int maxIndex = max_element(mat[mid].begin(), mat[mid].end()) - mat[mid].begin();
            if(mid==0 || mat[mid][maxIndex]>mat[mid-1][maxIndex]){
                if(mid==m-1 || mat[mid][maxIndex]>mat[mid+1][maxIndex]){
                    return {mid, maxIndex};
                }
                else{
                    up = mid+1;
                }
            }
            else{
                down = mid-1;
            }
        }
        return {};
    }
};

搜索二维矩阵

74. 搜索二维矩阵

题目描述:
  给你一个满足下述两条属性的 m x n m x n mxn 整数矩阵:

  每行中的整数从左到右按非严格递增顺序排列。
  每行的第一个整数大于前一行的最后一个整数。
  给你一个整数 t a r g e t target target ,如果 t a r g e t target target 在矩阵中,返回 t r u e true true ;否则,返回 f a l s e false false

代码:

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int m = matrix.size(), n = matrix[0].size();
        int left = 0, right = m-1;
        while(left<=right){
            int mid = left + (right-left)/2;
            if(matrix[mid][0]>target){
                right = mid-1;
            }
            else{
                left = mid+1;
            }
        }
        int index = right;
        if(index==-1) return false;

        auto it = lower_bound(matrix[index].begin(), matrix[index].end(), target);
        if(it==matrix[index].end() || *it!=target) return false; 
        return true;
    }
};

240. 搜索二维矩阵 II

题目描述:
  编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
  每行的元素从左到右升序排列。
  每列的元素从上到下升序排列。

代码:

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int m = matrix.size(), n = matrix[0].size();
        int x = 0, y = n-1;
        while(x<m && y>=0){
            if(matrix[x][y]==target) return true;
            if(target<matrix[x][y]){
                y--;
            }
            else{
                x++;
            }
        }
        return false;
    }
};

参考文献

[1] 灵茶山艾府.分享丨【题单】二分算法(二分答案/最小化最大值/最大化最小值/第K小)
[2] 灵茶山艾府.【视频讲解】二分查找总是写不对?三种写法,一个视频讲透!(Python/Java/C++/C/Go/JS)
[3] 灵茶山艾府.二分答案,附题单(Python/Java/C++/C/Go/JS/Rust)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值