二分法
二分法(Binary Search)主要用于在有序数组中搜索恰好满足某一边界条件的元素。如果题目所求的答案具有是或者不是两种状态,则说明其可能是二分法的题目。同时还需要注意蓝桥杯通常不会直接出题考察二分法,而是将其内嵌到其他算法中联合考察,该算法往往应用于快速在某一定义域区间中枚举出正确答案。
一、算法模板
(一)调用库函数
Python标准库 bisect 中有二分查找函数,可以根据给定的有序数组和值直接求出二分查找的结果下标
from bisect import bisect_left, bisect_right
nums = [1,2,3,4,5,5,5,6,7,8]
# bisect_left() 返回列表中所有元素值等于所给值的元素的最小下标(左侧插入点)
index_1 = bisect_left(nums,5) # index_1 = 4
# bisect_right() 返回列表中所有元素值等于所给值的元素的最大下标加一(右侧插入点)
index_2 = bisect_right(nums,5) # index_2 = 7
使用 bisect 库函数时应确保所提供的数组是有序数组
(二)二分法求最大解
border = 4.3
def check(num):
if num <= border:
return True
else:
return False
# binary search
nums = [1,2,3,4,5,6,7,8,9]
left = 0
right = len(nums) - 1
while left < right:
# 注意mid要加一,不然会出现死循环
mid = (left + right + 1) >> 1
if check(nums[mid]):
# mid is valid
left = mid
else:
# mid is not valid
right = mid-1
print(nums[left]) # output:4
(三)二分法求最小解
border = 6
def check(num):
if num <= border:
return True
else:
return False
# main part
nums = [1,2,3,4,5,6,7]
left = 0
right = len(nums) - 1
mid = (left + right) >> 1
while left < right:
mid = (left + right) >> 1
if check(nums[mid]):
# mid is not valid
left = mid + 1
else:
right = mid
print(nums[left]) # output:7
二、应用实例
例题 1:求阶乘(蓝桥杯14届省赛)
题目描述:
满足 N ! N! N! 的末尾恰好有 K K K 个0的最小的 N N N 是多少?输出这个数。如果这样的 N N N 不存在,输出-1
输入格式:
输入一行,包含一个整数,表示k
输出格式:
输出一行,包含一个整数
提示:
对于 30 % 30\% 30% 的数据, 1 ≤ K ≤ 1 0 6 1 \leq K \leq 10^6 1≤K≤106,
对于 100 % 100\% 100% 的数据, 1 ≤ K ≤ 1 0 18 1 \leq K \leq 10^{18} 1≤K≤1018
代码示例:
# 暴力做法,只能通过40%的样例
# 0只能通过乘以10的倍数或5的倍数获得,因为2的倍数远多于5的倍数,所以不用考虑
# 100,1200之类的数需要特别处理
from sys import exit
k = int(input())
if k < 0:
print(-1)
exit()
# 从5开始,以5为步长搜索5的倍数和10的倍数
n = 0 # 记录当前遍历到的因数
cur = 0 # 记录当前所得到的0的个数
while cur < k:
n += 5
# 先将n末尾所有的0转移到cur上
temp = n
while temp%10 == 0:
temp //= 10
cur += 1
# 然后统计temp中5的个数
while temp%5 == 0:
temp //= 5
cur += 1
if cur == k:
print(n)
else:
print(-1)
注意到对于一个给定的
n
!
n!
n!,末尾0的个数 = 从1到n各个数字的约数中5的个数 = k
k
=
n
5
1
+
n
5
2
+
n
5
3
+
.
.
.
k = \frac n {5^1} + \frac n {5^2} + \frac n {5^3} + ...
k=51n+52n+53n+... 可以高效地求出指定n!的末尾0个数,所以可以使用二分法提高查找效率。
# Binary Search Optimize
# 注意到末尾0的个数随n的增大只会增大,不会减小,且结果的末尾0个数有大于等于k和小于k两种状态,所以可以使用二分法高效求解
def check(n):
# 求出 n! 末尾0的个数
res = 0
# 这个过程的实质是求出构成阶乘的数字中所有5的倍数,25的倍数,125的倍数...
while n:
n //= 5
res += n
return res
# binary search main part
k = int(input())
left = 1
right = 10**19
while left < right:
mid = (left+right)//2
if check(mid) < k:
left = mid+1
else:
# mid 可能是正确答案,所以不能舍弃
right = mid
# left = right
if check(left) == k:
# 存在解
print(left)
else:
print(-1)
例题 2:k倍区间(蓝桥杯13届省赛)
题目链接:k倍区间
# 前缀和 + 取余 = 60%通过 + 40%超时
from copy import copy
n,k = map(int,input().split())
a = [int(i) for i in input().split()]
ans = 0
# 首先用原数组求单位长度区间的区间和是否满足条件
for i in range(n):
if a[i] >= 0 and a[i] % k == 0:
ans += 1
# 将a数组转化成前缀和数组再次求解
a = [0] + a
for i in range(1,n+1):
a[i] += a[i-1]
# 因为我们只关心是否可以整除k,所以在计算前缀和的时候直接对k取余
# 余数相同的前缀和相减所得结果一定可以被 k 整除,但因为存在负数,所以还需要判断两个前缀和的大小关系
b = a.copy()
for i in range(1,n+1):
b[i] = a[i]%k
for left in range(1,n):
for right in range(left+1,n+1):
if b[right] == b[left-1]:
# 说明区间和是k的倍数,但可能是负数,所以还需要比较两端区间和的大小
if a[left-1] <= a[right]:
ans += 1
print(ans)
# 前缀和 + 取余 + 排序 + 二分
from bisect import bisect_right
from collections import defaultdict
n,k = map(int,input().split())
a = [int(i) for i in input().split()]
total = 0 # 前缀和
ans = 0 # 答案
d = defaultdict(list)
d[0].append(0)
# 注意一定要在离散化的同时进行二分搜索,因为各个前缀和的相对先后顺序不能被打乱
for i in range(n):
total += a[i]
# 计算出当前前缀和的余数
temp = total % k
# 确保余数均为正数以便操作(因为正负两种形式在事实上是等价的)
if temp < 0:
temp += k
# 从当前元素的左侧所有和当前前缀和余数相等的元素中找出所有前缀和小于当前值的数量
# 排序 + 二分 提高查找效率
index = bisect_right(d[temp], total) # 比较的对象是各个区间的前缀和
ans += index
# 将新的前缀和插入有序数组,保证数组始终有序
d[temp].insert(index, total)
print(ans)