[python刷题模板] 二分模板

一、 算法&数据结构

1. 描述

python3中提供了bisect.bisect_left()和bisect.bisect_right()函数使用,但是只有python3.10及以上才支持key参数。
因此自己实现一个带key参数的lower_bound。这里采用cpp的命名。
题目如果求'最大值最小化'/'最小值最大化',一般都是二分答案。
如果只让求最小值/最大值,有时也可以想想二分。
  • 有的题从题干条件推答案非常难。这时反过来思考,如果知道答案,反过来验证题干中的条件反而很简单。
  • 这时可以枚举答案,看看哪个答案推出的条件正好是题干给出的那个。
  • 如果暴力枚举会炸,用二分就能从O(n)降到O(lgn)。
  • 用二分的条件是f(x)相对于x是单调的。单调递增或递减都可以,相应的调整二分方向即可。

  • 二分由于边界/中间位置和>/</>=/<=的条件交织,虽然代码不长,但写出一个没有bug的二分其实不是一件容易事(据说:第一篇二分搜索论文是 1946 年发表,然而第一个没有 bug 的二分查找法却是在 1962 年才出现,中间用了 16 年的时间。)
  • 因此最好的方法是:背!(或者CV

这里简单描述一下二分的原理,和代码书写技巧。
  • 很多介绍二分的时候会把目标区间以f(x)分成红蓝两部分,因此经常可以看到一些模板代码里,判断条件写成is_blue。
  • 而我习惯把目标区间以f(x)分成False和True的左右两部分,形如: [False,..., False,True,..True]。判断条件写成is_right/is_true。感觉这样表达的信息更多些(且明确)。
  • 由于f(x)对于x连续且单调,则有:
    1. 对于任意f(x)==True, f(x+1)必为True。 (x+1<hi)
    2. 对于任意f(x)==False,f(x-1)必为False。 (x>0)
  • 那么二分的原理就是不断尝试缩小剩余目标区间,直到区间空:
    • mid=(lo+hi)/2,若f(mid)==False,则证明mid-1以及以左都为False(或者说left),左边界可以右移到mid;
    • 同理,若f(mid)=True,则证明mid+1以右都为True(或者说right),右边界可以左移到mid。
    • 如果想不明白,再给一点提示,关键点在于:不看剩余目标区间[lo,hi]之间的元素有什么性质,而要看区间外边的元素有什么性质。
    • 性质就是:lo左边都是False,hi右边都是True。
    • 那么循环完后,hi的位置就是第一个True。

  • 把握以上循环不变量后,写开/闭区间都是可以,我个人推荐开区间写法。因为根据上边的说法,移动lo/hi的目的是将左边的False/右边的True移除出剩余目标区间,那么开区间的话就可以直接让lo/hi=mid,直接就把mid移除了,非常舒服。

2. 复杂度分析

  1. 查询query, O(log2n)

3. 常见应用

  1. 二分答案,用较低时间(O(n))验证目标位置的True/False。
  2. 小数逼近的题目。这种题目可以设置好计算方法后,直接循环固定次数,比如60次:while cnt>0:cnt-=1。

4. 常用优化

  1. python的bisect_left非常好用,但是只支持单调递增的;如果我们的目标函数是单调递减的,这时可以调整key和x的正负性来变相使用这个方法。
    • 但既然用is_right封装就没有这个问题,让right部分全返回True即可。
  2. 在单调区间内,除了找lower_bound(>=),还能找<=/>/<,四种情况都可以用lower_bound来代替:
    • : >=x: lower_bound(f(x))本身就是>=,或者叫 第一个为True的位置。
    • : > x : 可以转化为lower_bound(f(x+1))。(这个其实很少用在二分答案的题目上,因为你可以直接把目标x定在x+1上;一般都是在有序数组上找目标位置,这就建议直接用自带的bisect_right。)
    • : < x : 找最后一个False,其实就是lower_bound(f(x))-1。
    • : <=x: 找第一个True或最后一个False,可以用lower_bound(f(x+1))-1.(同样几乎不会用)
  3. 如果是很容易确定上界,建议直接开1e16甚至1e18,不要怂就是干,二分顶多60来次。
  4. 亲测,如果不套板子,手写while二分,效率比套板子高20%左右,这可能是因为py的func call比较慢。
  5. 如果是有序数组上找数字,那直接bisect_left,千万别客气,库函数是c实现的。

二、 模板代码


0. lower_bound(lo: int, hi: int, key):返回目标区间第一个为True的位置。

  • 注意,虽然实现是开区间的,但使用时接口是[左闭,右闭]。
    CV版
def lower_bound(lo: int, hi: int, key):  # 注意左右区间都是闭
    lo, hi = lo-1,hi+1  # 开区间(lo,hi)
    while lo + 1 < hi:  # 区间不为空
        mid = (lo + hi) >> 1  # py不担心溢出,实测py自己不会优化除2,手动写右移
        if key(mid): hi = mid  # is_right则右边界向里移动,目标区间剩余(lo,mid)            
        else: lo = mid  # is_left则左边界向里移动,剩余(mid,hi)            
    return hi

详细版

def lower_bound(lo: int, hi: int, key):
    """由于3.10才能用key参数,因此自己实现一个。
    :param lo: 二分的左边界(闭区间)
    :param hi: 二分的右边界(闭区间)
    :param key: key(mid)判断当前枚举的mid是否应该划分到右半部分。
    :return: 右半部分第一个位置。若不存在True则返回hi+1。
    虽然实现是开区间写法,但为了思考简单,接口以[左闭,右闭]方式放出。
    """
    lo -= 1  # 开区间(lo,hi)
    hi += 1
    while lo + 1 < hi:  # 区间不为空
        mid = (lo + hi) >> 1  # py不担心溢出,实测py自己不会优化除2,手动写右移
        if key(mid):  # is_right则右边界向里移动,目标区间剩余(lo,mid)
            hi = mid
        else:  # is_left则左边界向里移动,剩余(mid,hi)
            lo = mid
    return hi

1. 二分答案,<x模型:lower_bound()-1,找最后一个False。

在这里插入图片描述
这题贪心可以做到O(n),但二分跑得比贪心还快,我不理解,但大为震撼。

  • 先手玩一下,发现只需要从n//2的位置开始处理,给中位数增长的时候要确保它不会比后边相邻的数大,否则要同步增长后边那个/些数。
  • 令f(x)表示是否可以用至多k次做到中位数x。
  • 显然若f(x)=False,则f(x+1)必为False; 若f(x)=True,则f(x-1)必为True。
  • 我们把上一行的True/False交换,就变成了正常的模型,目标是找交换后的最右一个False。
  • 那么枚举目标x后,则只需要从中位数开始向后,每个数都增加到x,看看k够不够用即可。够用就返回False(代表它是左半部分)。(注意交换过了,不要直觉。)
# Problem: C. Maximum Median
# Contest: Codeforces - Codeforces Round 577 (Div. 2)
# URL: https://codeforces.com/problemset/problem/1201/C
# Memory Limit: 256 MB
# Time Limit: 2000 ms

import sys

RI = lambda: map(int, sys.stdin.buffer.readline().split())
RILST = lambda: list(RI())


def lower_bound(lo: int, hi: int, key):
    """由于3.10才能用key参数,因此自己实现一个。
    :param lo: 二分的左边界(闭区间)
    :param hi: 二分的右边界(开区间)
    :param key: key(mid)判断当前枚举的mid是否应该划分到右半部分。
    :return: 右半部分第一个位置。若不存在True则返回hi。
    虽然实现是开区间写法,但为了和切片/数组下标统一,接口依然以[左闭,右开)方式放出。
    """
    lo -= 1  # 开区间(lo,hi)
    while lo + 1 < hi:  # 区间不为空
        mid = (lo + hi) >> 1  # py不担心溢出,实测py自己不会优化除2,手动写右移
        if key(mid):  # is_right则右边界向里移动,目标区间剩余(lo,mid)
            hi = mid
        else:  # is_left则左边界向里移动,剩余(mid,hi)
            lo = mid
    return hi


#   156 ms
def solve():
    n, k = RI()
    a = RILST()
    a.sort()
    a = a[n // 2:]

    def is_right(x):
        s = k
        for i, v in enumerate(a):
            if v < x:
                s -= x - v
            else:
                break
        return s < 0

    print(lower_bound(0, k + a[0] + 1, key=is_right) - 1)


if __name__ == '__main__':
    solve()

2. 二分答案,>=x模型:lower_bound,找第一个True。

在这里插入图片描述

这题是lc上的题目,py版本是3.11,实际上支持key参数,出于模板的目的依然把题目放进来。
  • 令f(x)为最大差值为x时,是否能找到至少p对下标。
  • 显然这个是标准的左False右True的序列,直接套板子即可。
def lower_bound(lo: int, hi: int, key):
    """由于3.10才能用key参数,因此自己实现一个。
    :param lo: 二分的左边界(闭区间)
    :param hi: 二分的右边界(开区间)
    :param key: key(mid)判断当前枚举的mid是否应该划分到右半部分。
    :return: 右半部分第一个位置。若不存在True则返回hi。
    虽然实现是开区间写法,但为了和切片/数组下标统一,接口依然以[左闭,右开)方式放出。
    """
    lo -= 1  # 开区间(lo,hi)
    while lo + 1 < hi:  # 区间不为空
        mid = (lo + hi) >> 1  # py不担心溢出,实测py自己不会优化除2,手动写右移
        if key(mid):  # is_right则右边界向里移动,目标区间剩余(lo,mid)
            hi = mid
        else:  # is_left则左边界向里移动,剩余(mid,hi)
            lo = mid
    return hi

class Solution:
    def minimizeMax(self, nums: List[int], p: int) -> int:
        n = len(nums)
        nums.sort()
        
        def ok(x):
            s = 0
            i  = 0
            while i < n-1:
                if nums[i+1] - nums[i] <= x:
                    s += 1
                    i += 1
                i += 1
            return s >= p 
        
        return lower_bound(0,10**9+1,key=ok)
        # return bisect_left(range(10**9+1),True,key=ok)     ## lc py支持

三、其他

  1. 如果卡常,尝试把板子展开,避免func call。效率快很多。
  2. 能用库函数自带的bisect_left就用,只要不带key方法,走的就是c语言,极快。
  3. 对于bisect_left由于第一个参数是数组,可以构造range(0,hi),这个玩意可以开到1e15都不会tle,估计是lazy的实现。但这时key一定返回bool类型(不要具体数字),否则会被正负号烦死。
    • 注意,range第一个参数千万一定要设置成0,如果要设置二分下界,请使用bisect_left(range(0,hi),True,lo=123,key=ok)。
    • 若使用了range(123,hi),那么返回的是这个range里ans的下标,即:若正确答案是124,会返回2。已经在这里失去了很多debug时间了。

四、更多例题

  • 待补充

五、参考链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值