前言
二分法的题单可以刷 灵茶山艾府 的题单分享丨【题单】二分算法(二分答案/最小化最大值/最大化最小值/第K小)。本文是该分享的学习笔记。
二分法的核心思想在于利用 有序性 ,通过反复将搜索区间 一分为二 ,逐步缩小查找范围,从而提高查找效率。
二分法的使用场景:待查找的数据必须是有序的。
二分查找
算法思路
1.定义左右指针left, right,表示一个区间 Q,Q 区间表示目标 target 在这个区间范围内。
2.当区间 Q 内还有元素时,不断移动 left 和 right 来缩小区间 Q 的范围,直到区间 Q 没有元素,退出循环。
3.需要注意每次移动 left 和 right 一定要使 Q 能够缩小,否则会陷入死循环。
4.根据移动 left 和 right 时书写的条件,判断当循环结束后 left 和 right 所表示的具体含义,根据这个具体含义来确定最后返回的到底是什么值。
二分法基本模板
//找到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;
}
解释:
-
使用 [left, right] 来表示区间 Q, 初始时,Q 为整个数组区间 [0,n-1],因此定义 left = 0 , right = n-1 表示初始时的区间 Q。
-
当 left <= right 时,区间 [left, right] 还有元素,因此,此时 while循环 里的条件是 left <= right。
-
right 更新时不能更新为 mid,因为当 left = right,nums[mid] = target 时,mid = right, 则 right 的更新不会缩小 Q 区间,造成死循环。left 类似。
-
在一次移动 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[mid−1+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, 这就是灵神说的循环不变量。 -
同样,对于更新后的 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[left1−1]=nums[mid−1+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[left1−1]<target
所以当循环结束时,此时的 left 左边的所有元素都会 小于 target。 -
所以循环结束时 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);
提醒:
- 使用 lower_bound 和 upper_bound 函数之前需要确保容器中的元素有序。
- 当数组为 空 时,此时 it = nums.begin() = nums.end(), 对 it 本身或者前一个元素和后一个元素的访问会非法。
- 当不存在满足条件的下标时,it = nums.end()。
- 自定义比较函数如果使用函数体书写且写在类的内部,要加上 static 关键字,因为成员函数(非静态)隐含地接受一个指向其所属类实例的this指针作为第一个参数。因此,如果尝试将成员函数作为比较函数传递给算法时,算法的参数与成员函数的期望参数不匹配。
二分查找的几种情况转换
- 查找第一个大于等于 target 的元素的下标
auto it = lower_bound(nums.begin(), nums.end(), target);
int index = it - nums.begin();
- 查找第一个大于 target 的元素的下标
auto it = upper_bound(nums.begin(), nums.end(), target);
int index = it - nums.begin();
- 查找最后一个小于等于 target 的元素的下标
//等价于查找第一个大于 target 元素的前一个元素
auto it = upper_bound(nums.begin(), nums.end(), target);
int index = it - nums.begin() - 1;
- 查找最后一个小于 target 的元素的下标
//等价于查找第一个大于等于 target 元素的前一个元素
auto it = lower_bound(nums.begin(), nums.end(), target);
int index = it - nums.begin() - 1;
二分查找有关题目的思路
由于和二分查找有关的题目可以使用内置函数来解决查找的问题,所以解决这一类题目的关键就在于:
- 判断数组中是否存在有序的部分。
- 判断有序部分是否能利用,以及如何利用。
- 理清答案和返回的各个迭代器之间的关系
标准二分查找
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. 两个数组间的距离值
题目描述:
给你两个整数数组 arr1 , arr2 和一个整数 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. 咒语和药水的成功对数
题目描述:
给你两个正整数数组 spells 和 potions ,长度分别为 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;
}
};
设计数据结构(注意到题目潜在描述中的有序信息)
- 思考怎样利用有序性构造数据结构,从而使用二分法来查找答案。C++内置二分查找的函数返回的是数组迭代器,要确定答案和所构造的数据结构的下标有关,还是和迭代器对应的数据有关。
- 注意到一些会严格自增的变量,比如时间等。
2080. 区间内查询数字的频率
题目描述:
请你设计一个数据结构,它能求出给定子数组内一个给定值的频率。
子数组中一个值的 频率 指的是这个子数组中这个值的出现次数。
请你实现 RangeFreqQuery 类:
RangeFreqQuery(int[] arr) 用下标从 0 开始的整数数组 arr 构造一个类的实例。
int query(int left, int right, int value) 返回子数组 arr[left…right] 中 value 的 频率 。
一个 子数组 指的是数组中一段连续的元素。arr[left…right] 指的是 nums 中包含下标 left 和 right 在内 的中间一段连续元素。
思路:
在 [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<xansx≥xans
由于 check(x) 具有单调性,所以我们可以用二分法来求得答案
k
a
n
s
k_{ans}
kans。二分答案时,选择合适的 left 和 right 作为初始值可以有效的缩小二分查找的范围。
定义的 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=0∑n⌊time[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<totalTripslimitx≥totalTrips
由于 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=0∑n⌈xnums[i]⌉=i=0∑n⌊xnums[i]+k−1⌋=i=0∑n⌊xnums[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,limitx≤thresholdlimitx>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篇论文至少分别引用了x次else.
发现当 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
k(0<=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[n−1],nums[0],nums[1],...,nums[k−1]](下标 从 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)
k(0<=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[n−1],nums[0],nums[1],...,nums[k−1]](下标 从 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[n−1]] 旋转一次 的结果为数组
[
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[n−1],a[0],a[1],a[2],...,a[n−2]] 。
给你一个元素值 互不相同 的数组
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,mid−1] 内 (闭区间写法需要判断
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[mid−1]>nums[mid],那么往
m
i
d
−
1
mid-1
mid−1 走一定会有一个峰值; 否则
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.length−1 的
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[i−1]<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.length−1]
给定一个山脉数组
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)