字节青训营 |二分查找、二分答案

二分查找

二分查找也称折半查找(Binary Search),是一种在有序数组中查找某一特定元素的搜索算法。

  • 时间复杂度:

    • 搜索——O(log N)(对于长度为N的序列,每进行一次查找,查找区间长度会缩短一半)
    • 插入——O(N)
  • 二分查找步骤:

  1. 候选区间[left,right]
  2. 不断循环,直至区间满足特定条件
    • 计算中点 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 * (单次验证当前值是否满足条件的复杂度))

  • 步骤:

  1. 确定初始范围[left,right] ,答案ans

  2. left <= right时:

    • mid = (left + right) // 2

    • check(mid):判断mid是否合法:(定义check函数,什么时候是合法,根据题目条件来确定)

    • 如果合法:更新ans = mid

    • 根据合法调整左右区间,调整策略为二选一: left = mid + 1right = 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 个小朋友,分配需要遵循以下规则:

  1. 每个小朋友必须且只能得到一种糖果(不能同时得到两种糖果)
  2. 每个小朋友至少要得到一个糖果
  3. 每种糖果至少要分给一个小朋友
  4. 为了尽可能公平,小M希望分到糖果最少的小朋友能得到尽可能多的糖果

请你帮助小M计算:在满足上述所有条件的前提下,分到糖果最少的小朋友最多能得到多少个糖果?

输入格式

  • 输入包含三个整数 n, a, b
  • 其中 n 表示小朋友的数量,a 和 b 分别表示两种糖果的数量

输出格式

  • 输出一个整数,表示在最优分配方案下,得到糖果最少的小朋友能得到的最大糖果数
  • 如果无法按要求分配,输出 0

解题思路

  1. 特殊情况

    • 如果 n > a + b,则无法满足每个小朋友至少得到一个糖果的条件,直接返回 0
    • 如果 n = a + b,则刚好每个小朋友至少得到一个糖果,直接返回 1
  2. 二分查找边界

    • 左边界left = 1,因为每个小朋友至少要得到一个糖果。
    • 右边界right = (a + b) // n,因为每个小朋友最多能得到的糖果数是 (a + b) // n
  3. check:检查当前的 mid 是否是一个可行的分配方案。

    • 计算每个小朋友至少mid个糖果时,糖果a、b分别可以分给几个小朋友numa = a//mid , numb = b//mid
    • 检查是否满足每种糖果至少要分给一个小朋友numa > 0 且 numb > 0
    • 检查是否能将 ab 分配给 n 个小朋友,使得每个小朋友至少得到 mid 个糖果。
  4. 区间调整

    • 如果当前mid合法,则目标值在右区间,调整left
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名教职员工对一名学生进行评估所需的时间(以分钟为单位)。

教职员工需要对所有学生进行评估,每名教职员工在任意时刻只能评估一名学生。评估完成后,教职员工可以立即开始评估下一名学生。你的任务是找到在这些条件下评估所有学生所需的最短总时间。

解题思路

  1. 特殊情况

    • 如果M = 1, 只有一名教职工评估所有学生,返回N*max(A)
  2. 初始化边界

    • 左边界left = min(A),即最快评估一个学生的时间。
    • 右边界right = N * max(A),即最慢评估所有学生的时间。
  3. check

    • 计算在 mid 时间内每个教职员工可以评估的学生数量。
    • 如果总和大于等于 N,则返回 True,否则返回 False
  4. 区间调整

    • 如果当前mid合法,则目标值在左区间,调整right
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 天内全部送达。

解题思路

  1. 初始化边界

    • 左边界left = max(weights),即单个包裹的最大重量。
    • 右边界right = sum(weights),即所有包裹的总重量。
  2. check

    • 判断在给定的运载能力 mid 下,是否可以在 days 天内完成运输。
    • 遍历 weights 数组,累加当前的重量,如果超过 mid,则表示需要新的一天来运输,天数加一,并重置当前重量。
    • 最后判断所需的天数是否小于等于 days
  3. 区间调整

    • 如果当前mid合法,则目标值在左区间,调整right
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】

解题思路

子序列的定义:子序列是从原数组中选择一些元素,保持它们在原数组中的相对顺序,但不要求连续。

  1. 排序数组:对数组 a 进行降序排序。
  2. 二分查找:使用二分查找来确定最长子序列的长度。
  3. 检查函数:在 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。牛妹想知道,在保证稳固性的前提下,桥梁中最高的桥柱能有多高。你需要帮助牛妹计算桥梁最高的桥柱的高度。

解题思路

  1. 特殊情况

    • 如果 n < 2,显然无法搭建桥,直接返回 -1
    • 如果 abs(a - b) > n - 1,说明即使每根桥柱的高度差为 1,也无法满足条件,直接返回 -1
  2. 二分查找

    • 二分查找的范围可以为 [1,max(a, b) + n]ans=-1
  3. check

    • 判断最高高度为 mid 时,是否可以搭建出满足条件的桥。

    • 如果桥柱要达到最高,区间[a,b]是先递增到mid然后再递减的,此时可能有的搭建情况:

      1. [a,a+1,……,mid-1,mid,mid-1,……,b+1,b]
      2. [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想知道,自己最多可以制作多少次这道菜。

解题思路

  1. 二分查找

    • 二分查找的范围可以为 [0,(a+b+c)//4]
  2. check

    • 判断在给定的制作次数 mid 下,是否可以使用现有的材料 abc 来完成。
    • 计算需要的材料 need_a = 2 * midneed_b = 2 * mid
    • 计算剩余的材料 remain_a = a - need_aremain_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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值