目录
如果还没有接触过二分算法的核心思想,可以先去看看二分的基本概念
二分的基本算法思想:
def binary_search(n): # 二分查找基础概念算法
# 初始化搜索范围的下界为0
low = 0
# 初始化搜索范围的上界为列表长度,注意这里应该是len(ls) - 1,因为Python列表索引是从0开始的
high = len(ls) - 1
# 当搜索范围的下界小于上界时,继续查找
while low < high:
# 计算中间索引
mid = (low + high) // 2
# 如果中间索引的值等于要查找的数n,则返回中间索引对应的元素
# 注意这里应该是ls[mid]而不是mid,因为我们要比较的是列表中的元素而不是索引,切记
if ls[mid] == n:
# 找到了
return ls[mid]
# 如果要查找的数n小于中间索引mid的值,则在上半部分继续查找
elif n < ls[mid]: # 同样是使用ls[mid]来比较元素
high = mid - 1
# 如果要查找的数n大于中间索引mid的值,则在下半部分继续查找
else:
low = mid + 1
OK,二分算法的基本思想你已经掌握了,上真题
题目分析:
这道题理解起来其实不难,核心点就是我们要使转换前金属个数整除转换后的金属个数的转换效率保持在一个值内。
打个比方假如转换前的金属个数为5,我们想让让他的转换后的个数为2,当V(转换效率,以下同意)为1,很明显5//1=5即转换后金属个数也为5,当V=2,5//2=2,也就是我们两个金属分一组转换成一个高级金属,剩下一个不够转换了。当V=3,5//3=1,只能转换一个了,将其中三个拿出来后剩下两个不足以转换,再往后肯定更不可能了,根据这个思想,我们来尝试做题
如果下面的代码部分没有看懂,可以参考大佬的视频讲解:[蓝桥杯]真题讲解:冶炼金属(暴力+二分)
首先来看这道题的暴力解法:
# 读取输入数据个数
n = int(input())
# 初始化左边界为0,右边界为正无穷大
l, r = 0, float("inf")
# 遍历每一个数据组
for i in range(n):
# 读取数据并转换为整数列表
data = list(map(int, input().split()))
# 初始化存储结果的列表,用于存储所有满足满足----普通金属A // 特殊金属B == 转换效率V 的情况
res = []
# 计算可能的倍数的最小值和最大值
# 使用 (data[1]+1) 和 (data[1]-1) 是为了减少寻找次数,避免了我们从1开始循环,因为我们的目的是找到离转换效率V最接近的值,即(V-1和V+1)
# mn,mm表示离目标最接近的值的区间取值范围
mn, mx = data[0] // (data[1] + 1), data[0] // (data[1] - 1)
# 遍历可能的倍数范围
for i in range(mn, mx + 1):
# 检查当前倍数i是否满足整除data[1]次后剩下的部分为0的条件
if data[0] // i == data[1]:
# 如果满足条件,则将其添加到结果列表中
res.append(i)
# 更新左边界为当前结果中的最大值和已有左边界中的较大值
l = max(l, res[0])
# 更新右边界为当前结果中的最小值和已有右边界中的较小值
r = min(r, res[-1])
# 打印最终结果的左边界和右边界
print(l, r)
只对了40%的案例,剩下的超时了
正确解法:二分
# 用于检查mid是否满足条件:data[1] < data[0] // mid ,求转换效率V的下限值
def check_min(mid):
# 如果data[1]小于data[0]除以mid的商,则返回False,否则返回True
if data[1] < data[0] // mid:
return False
return True
# 用于检查mid是否满足条件:data[1] > data[0] // mid ,求转换效率V的上限值
def check_max(mid):
# 如果data[1]大于data[0]除以mid的商,则返回False,否则返回True
if data[1] > data[0] // mid:
return False
return True
# 从用户处读取一个整数n,表示将要输入的数据组数
n = int(input())
# 初始化搜索范围,left表示左边界,right表示右边界,用于存储最终结果
left, right = 0, int(10e9) # 这里right设置为一个很大的数,因为问题中的上界为10e9
# 遍历每一组数据
for i in range(n):
# 读取当前数据组,并转换为整数列表
data = list(map(int, input().split()))
# 初始化当前数据组的搜索范围
l, r = 1, int(10e9) # 这里l设置为1,r同样设置为一个很大的数
# 使用二分查找寻找满足check_min函数的最小mid值
while l < r:
# 计算中间值,这里使用右移运算符相当于整除2
mid = l + r >> 1
# 如果mid满足check_min条件,则更新右边界为mid
if check_min(mid):
r = mid
# 否则更新左边界为mid+1
else:
l = mid + 1
# 更新全局左边界为当前数据组左边界和全局左边界中的较大值
left = max(left, l)
# 重新初始化当前数据组的搜索范围
l, r = 0, int(10e9) # 这里l设置为0,因为我们要找的是满足check_max条件的最大mid值
# 使用二分查找寻找满足check_max函数的最大mid值
while l < r:
# 计算中间值,注意这里使用右移运算符加1是为了确保mid不会等于l,从而能够继续搜索
mid = l + r + 1 >> 1
# 如果mid满足check_max条件,则更新左边界为mid
if check_max(mid):
l = mid
# 否则更新右边界为mid-1
else:
r = mid - 1
# 更新全局右边界为当前数据组右边界和全局右边界中的较小值
right = min(right, l)
# 打印最终的左边界和右边界
print(left, right)
为什么第二个while里mid=l+r+1 // 2 呢,和第一个while不一样啊,这很奇怪啊。别急,慢慢听我胡说八道。
核心点还是在返回true的情况下,一个是L=mid,一个是R=mid,这俩有啥区别吗?当然有!
因为我们的整除是向下取整,假设L=28,R=29,如果是L=mid的话就会一直执行L=mid卡在L=28,R=29导致死循环L<R的条件始终不满足退出循环的条件,而后者mid=28,然后R的值就会更新为mid的值28,此时L=R=28,跳出循环体
想要避免这个问题也很简单,那就是每次取中值的时候多加一个1,就可以完美解决了
按照上面的理解不难总结出:死循环的情况只会出现在返回true的L = mid上而不会出现在返回true的R = mid上
100%通过了
通过以上的例子,我们还可以总结出类似二分查找类型题目的通项公式:
# 二分题目模板
# check函数是核心
def check(x):
pass
# 假设x为答案
# 题目一般有有个约束条件
# 如果通过某种手段使得在x的条件下存在符合约束条件的解
# 那么就是可行解
# 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用,把中间点放在左半边,不管三七二十一先用这个,做题过程中再修改
def bsearch_1(l, r):
while l < r:
mid = l + r >> 1 # 取中间值
if check(mid): # 判断是否满足某种性质
r = mid # 更新区间
else:
l = mid + 1
return l
# 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用,把中间点放在左半边(比如左边是合法的,右边是不合法的,此时mid属于合法的那一边)
def bsearch_2(l, r):
while l < r:
mid = l + r + 1 >> 1 # 取中间值
if check(mid): # 判断是否满足某种性质
l = mid # 更新区间
else:
r = mid - 1
return l