二分查找
二分查找也称折半查找(Binary Search),是一种在有序数组中查找某一特定元素的搜索算法。
-
时间复杂度:
- 搜索——
O(log N)
(对于长度为N的序列,每进行一次查找,查找区间长度会缩短一半) - 插入——
O(N)
- 搜索——
-
二分查找步骤:
- 候选区间[left,right]
- 不断循环,直至区间满足特定条件
- 计算中点
mid=(left+right)//2
,判断中点是否合法 - 如果合法,停止搜索
- 如果不合法,根据中点的计算结果调整
[left,right]
(如果目标在左半区间的话,就在左半区间[left,mid-1]
搜索。反之,就在右半区间[mid+1,right]
搜索。)
- 计算中点
bisect–数组二分查找算法
bisect模块 : 维护一个已排序列表,支持二分查找、二分插入
-
bisect_left(a, x, [lo=0, hi=len(a)]): —— 查找目标元素左侧插入点
- 查找有序列表 a 中,适合元素 x 插入的第一个位置
- 若 a 中存在元素 x,则返回第一个 (最左侧) x 的位置索引
- lo 和 hi 为可选参数,分别定义查找范围/返回索引的上限和下限,缺省时默认对整个序列查找。
-
bisect_right(a, x, [lo=0, hi=len(a)]) —— 查找目标元素右侧插入点
- 查找有序列表 a 中,适合元素 x 插入的最后一个位置
- 若 a 中存在元素 x,则返回最后一个 (最右侧) x 的位置索引 +1
- lo 和 hi 为可选参数,分别定义查找范围/返回索引的上限和下限,缺省时默认对整个序列查找。
-
insort_left(a, x, lo=0, hi=len(a)) —— 查找目标元素左侧插入点,并保序地 插入 元素
- 查找有序列表a,在第一个位置插入元素x,并维持其有序
-
insort_right(a, x, lo=0, hi=len(a)) —— 查找目标元素右侧插入点,并保序地 插入 元素
- 查找有序列表a,在最后一个位置插入元素x,并维持其有序
二分答案
题目所求答案(一般为整数)具有单调性质,采用猜答案+二分
---- 最大值最小化、最小值最大化
---- 最大/小合法值
-
时间复杂度:
O(logN * (单次验证当前值是否满足条件的复杂度))
-
步骤:
-
确定初始范围
[left,right]
,答案ans -
当
left <= right
时:-
mid = (left + right) // 2
-
check(mid):判断mid是否合法:(定义check函数,什么时候是合法,根据题目条件来确定)
-
如果合法:更新ans = mid
-
根据合法调整左右区间,调整策略为二选一:
left = mid + 1
,right = mid -1
-
代码模板(非递归)
def check(x):
# 判断x是否合法,合法返回True,不合法返回False
pass
def solution() -> int:
#根据题目信息设定答案可能出现的最小值left和最大值right,初始化最终答案ans
left, right , ans = 初始化
while left <= right:
mid = (left + right) // 2
if check(mid):
ans = mid #将ans设置为当前最新合法值mid
left = mid + 1 #调整搜索区间为[mid+1,right]
else:
right = mid - 1 #调整搜索区间为[left,mid-1]
return ans
糖果公平分配问题 —— 最小值最大化
问题描述
小M从商店买了两种糖果,第一种有 a 个,第二种有 b 个。现在她想要将这些糖果分给班上的 n 个小朋友,分配需要遵循以下规则:
- 每个小朋友必须且只能得到一种糖果(不能同时得到两种糖果)
- 每个小朋友至少要得到一个糖果
- 每种糖果至少要分给一个小朋友
- 为了尽可能公平,小M希望分到糖果最少的小朋友能得到尽可能多的糖果
请你帮助小M计算:在满足上述所有条件的前提下,分到糖果最少的小朋友最多能得到多少个糖果?
输入格式
- 输入包含三个整数 n, a, b
- 其中 n 表示小朋友的数量,a 和 b 分别表示两种糖果的数量
输出格式
- 输出一个整数,表示在最优分配方案下,得到糖果最少的小朋友能得到的最大糖果数
- 如果无法按要求分配,输出 0
解题思路
-
特殊情况:
- 如果
n > a + b
,则无法满足每个小朋友至少得到一个糖果的条件,直接返回0
。 - 如果
n = a + b
,则刚好每个小朋友至少得到一个糖果,直接返回1
。
- 如果
-
二分查找边界:
- 左边界:
left = 1
,因为每个小朋友至少要得到一个糖果。 - 右边界:
right = (a + b) // n
,因为每个小朋友最多能得到的糖果数是(a + b) // n
。
- 左边界:
-
check:检查当前的
mid
是否是一个可行的分配方案。- 计算每个小朋友至少mid个糖果时,糖果a、b分别可以分给几个小朋友
numa = a//mid , numb = b//mid
- 检查是否满足每种糖果至少要分给一个小朋友
numa > 0 且 numb > 0
- 检查是否能将
a
和b
分配给n
个小朋友,使得每个小朋友至少得到mid
个糖果。
- 计算每个小朋友至少mid个糖果时,糖果a、b分别可以分给几个小朋友
-
区间调整:
- 如果当前mid合法,则目标值在右区间,调整
left
- 如果当前mid合法,则目标值在右区间,调整
def solution(n: int, a: int, b: int) -> int:
if n > a + b : #无法满足每个小朋友至少得到一个糖果的条件,直接返回 `0`
return 0
if n == a + b : #每个小朋友刚好得到一个糖果,直接返回 `1`
return 1
left, right, ans = 1,(a+b)//n,1 #初始化left, right , ans
while left <= right: #二分查找
mid = (left + right) // 2
if check(mid,n,a,b):
ans = mid
left = mid +1
else:
right = mid -1
return ans
#判断mid是否合法
def check(mid:int,n:int,a:int,b:int) -> bool:
#计算每个小朋友至少mid个糖果时,糖果a、b分别可以分给几个小朋友
numa = a//mid
numb = b//mid
#每种糖果至少要分给一个小朋友,且糖果可以分给 n 个小朋友
if numa > 0 and numb > 0 and numa + numb >= n :
return True
return False
最短评估时间问题 —— 最小合法值
问题描述
小R在教室里有N
名学生和M
名教职员工。每个教职员工的评估速度不同,评估每个学生所需的时间不同。你得到一个数组A
,其中A[i]
表示第i
名教职员工对一名学生进行评估所需的时间(以分钟为单位)。
教职员工需要对所有学生进行评估,每名教职员工在任意时刻只能评估一名学生。评估完成后,教职员工可以立即开始评估下一名学生。你的任务是找到在这些条件下评估所有学生所需的最短总时间。
解题思路
-
特殊情况:
- 如果
M = 1
, 只有一名教职工评估所有学生,返回N*max(A)
- 如果
-
初始化边界:
- 左边界 :
left = min(A)
,即最快评估一个学生的时间。 - 右边界 :
right = N * max(A)
,即最慢评估所有学生的时间。
- 左边界 :
-
check:
- 计算在
mid
时间内每个教职员工可以评估的学生数量。 - 如果总和大于等于
N
,则返回True
,否则返回False
。
- 计算在
-
区间调整:
- 如果当前mid合法,则目标值在左区间,调整
right
- 如果当前mid合法,则目标值在左区间,调整
def solution(N: int, M: int, A: list) -> int:
if M == 1: #只有一名教职工评估所有学生
return N*max(A)
left, right ,ans = min(A), N * max(A), min(A) #初始化left, right , ans
while left <= right: #二分查找
mid = (left + right) // 2
if check(N,M,A,mid):
ans = mid
right = mid-1
else:
left = mid + 1
return ans
#判断mid是否合法
def check(N:int, A:list, mid:int) :
num = 0 #可处理的学生人数
for time in A:
num += mid // time #计算在 mid 时间内每个教职员工可以评估的学生数量,并求和
return num >= N
传送带包裹运输问题 —— 最小合法值
问题描述
小R需要在 days
天内将一批包裹从一个港口运送到另一个港口。传送带上的每个包裹的重量由数组 weights
表示,第 i
个包裹的重量为 weights[i]
。
每一天,小R按照包裹在 weights
中的顺序装载包裹,装载的总重量不会超过船的最大运载能力。为了在规定的天数内完成运输任务,小R希望知道船的最低运载能力是多少,才能确保所有包裹能够在 days
天内全部送达。
解题思路
-
初始化边界:
- 左边界 :
left = max(weights)
,即单个包裹的最大重量。 - 右边界 :
right = sum(weights)
,即所有包裹的总重量。
- 左边界 :
-
check:
- 判断在给定的运载能力
mid
下,是否可以在days
天内完成运输。 - 遍历
weights
数组,累加当前的重量,如果超过mid
,则表示需要新的一天来运输,天数加一,并重置当前重量。 - 最后判断所需的天数是否小于等于
days
。
- 判断在给定的运载能力
-
区间调整:
- 如果当前mid合法,则目标值在左区间,调整
right
- 如果当前mid合法,则目标值在左区间,调整
def solution(weights: list, days: int) -> int:
# 设 `left` 为单个包裹的最大重量,`right` 为所有包裹的总重量。
left, right, ans = max(weights), sum(weights), 1
while left <= right:
mid = (left + right) // 2
if check(mid,weights,days):
ans = mid
right = mid - 1
else:
left = mid + 1
return ans
def check(mid: int, weights: list, days: int) -> bool:
current_days = 1
current_weight = 0
for weight in weights:
if current_weight + weight > mid:
# 如果当前重量加上新包裹的重量超过mid,则需要新的一天
current_days += 1
current_weight = weight
else:
current_weight += weight
# 如果所需天数超过days,则返回False
if current_days > days:
return False
return True
平均数大于k的最长子序列 —— 最大合法值
问题描述
小U手里有一个由n个正整数组成的数组。她希望能够从中找到一个子序列,使得这个子序列的平均数大于一个给定的值k。你的任务是帮助小U找到这样的子序列,并且求出其中最长的子序列长度。如果无法找到平均数大于k的子序列,那么输出−1。【测试用例输出0】
解题思路
子序列的定义:子序列是从原数组中选择一些元素,保持它们在原数组中的相对顺序,但不要求连续。
- 排序数组:对数组
a
进行降序排序。 - 二分查找:使用二分查找来确定最长子序列的长度。
- 检查函数:在
check
函数中,计算前mid
个元素的平均数,并判断是否大于k
。
def solution(n: int, k: int, a: list) -> int:
a.sort(reverse=True)
left, right, ans = 1, n, 0
while left <= right:
mid = (left + right) // 2
if check(mid, k, a):
ans = mid
left = mid + 1
else:
right = mid - 1
return ans
def check(mid: int, k: int, a: list) -> bool:
return sum(a[:mid])/mid > k
桥梁最高高度 —— 最大合法值
问题描述
牛妹需要用 n 根桥柱搭建一座桥,第一根桥柱和最后一根桥柱的高度已经确定,分别为 a 和 b。为了保证桥梁的稳固性,相邻桥柱的高度差不能超过 1。牛妹想知道,在保证稳固性的前提下,桥梁中最高的桥柱能有多高。你需要帮助牛妹计算桥梁最高的桥柱的高度。
解题思路
-
特殊情况:
- 如果
n < 2
,显然无法搭建桥,直接返回-1
。 - 如果
abs(a - b) > n - 1
,说明即使每根桥柱的高度差为 1,也无法满足条件,直接返回-1
。
- 如果
-
二分查找:
- 二分查找的范围可以为
[1,max(a, b) + n]
,ans=-1
- 二分查找的范围可以为
-
check:
-
判断最高高度为
mid
时,是否可以搭建出满足条件的桥。 -
如果桥柱要达到最高,区间[a,b]是先递增到mid然后再递减的,此时可能有的搭建情况:
[a,a+1,……,mid-1,mid,mid-1,……,b+1,b]
[a,a+1,……,mid-1,mid,mid,mid-1,……,b+1,b]
-
总高度差为
(mid - a) + (mid - b)
,即2 * mid - a - b
。 -
需要确保这些高度差可以在
n
根桥柱中实现,因此总高度差不能超过n - 1
-
def solution(n: int, a: int, b: int) -> int:
if n < 2 or abs(a - b) > n - 1:
return -1
left, right, ans = 1,max(a,b) + n,-1
while left <= right:
mid = (left + right) // 2
if check(mid,n,a,b):
ans = mid
left = mid + 1
else:
right = mid - 1
return ans
def check(mid: int, n: int, a: int, b: int) -> bool:
return 2 * mid - a - b <= n - 1
小S的菜式制作—— 最大合法值
问题描述
小S准备做一道菜。为了做这道菜,小S需要 2 个材料a和 2 个材料b。现在小S有 a 个材料a,b 个材料b,以及 c 个万能个材料(每个万能食材可以替代一个材料a或者一个材料b)。小S想知道,自己最多可以制作多少次这道菜。
解题思路
-
二分查找:
- 二分查找的范围可以为
[0,(a+b+c)//4]
- 二分查找的范围可以为
-
check:
- 判断在给定的制作次数
mid
下,是否可以使用现有的材料a
、b
和c
来完成。 - 计算需要的材料
need_a = 2 * mid
和need_b = 2 * mid
。 - 计算剩余的材料
remain_a = a - need_a
和remain_b = b - need_b
。 - 计算需要补充的材料数量
need_c = max(0, -remain_a) + max(0, -remain_b)
。 - 判断
need_c
是否小于等于c
,即是否可以使用万能材料补充。
- 判断在给定的制作次数
def solution(a: int, b: int, c: int) -> int:
left, right, ans = 0,(a+b+c)//4,0
while left <= right:
mid = (left + right) // 2
if check(mid,a,b,c):
ans = mid
left = mid + 1
else:
right = mid - 1
return ans
def check(mid: int, a: int, b: int, c: int) -> bool:
return max(0,2*mid - a) + max(0,2*mid - b) <= c