[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连续且单调,则有:
- 对于任意f(x)==True, f(x+1)必为True。 (x+1<hi)
- 对于任意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. 复杂度分析
- 查询query, O(log2n)
3. 常见应用
- 二分答案,用较低时间(O(n))验证目标位置的True/False。
- 小数逼近的题目。这种题目可以设置好计算方法后,直接循环固定次数,比如60次:while cnt>0:cnt-=1。
4. 常用优化
- python的bisect_left非常好用,但是只支持单调递增的;如果我们的目标函数是单调递减的,这时可以调整key和x的正负性来变相使用这个方法。
- 但既然用is_right封装就没有这个问题,让right部分全返回True即可。
- 在单调区间内,除了找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.(同样几乎不会用)
- 如果是很容易确定上界,建议直接开1e16甚至1e18,不要怂就是干,二分顶多60来次。
- 亲测,如果不套板子,手写while二分,效率比套板子高20%左右,这可能是因为py的func call比较慢。
- 如果是有序数组上找数字,那直接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支持
三、其他
- 如果卡常,尝试把板子展开,避免func call。效率快很多。
- 能用库函数自带的bisect_left就用,只要不带key方法,走的就是c语言,极快。
- 对于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时间了。
淦
四、更多例题
- 待补充