例题 1:中位数(蓝桥杯 P2143)
| 项目 | 内容 |
|---|---|
| 链接 | https://www.lanqiao.cn/problems/2143/learning/ |
| 类型 | 二分答案 + 数学分析 |
| 核心 | 中位数性质、二分找分界点 |
题目描述
给定长度为 n 的数组 arr,定义操作:每次选择一个数 x,将数组中所有 < x 的数变为 x。求使数组中位数(排序后中间位置的数)最大的最小操作次数,并输出每个元素的最终值。
输入格式
n
arr[1] arr[2] ... arr[n]
输出格式
n 个整数,表示每个元素的最终值。
题解
关键观察 :最终数组是非递减的(因为每次操作把小的变大)。设最终中位数为 ans,则需要:
- 大于
ans的元素个数<=小于ans的元素个数
二分找 ans :check(x) 判断能否使中位数 >= x。
python
import bisect
n = int(input())
arr = list(map(int, input().split()))
a = sorted(arr)
def check(x):
# 大于x的元素个数 <= 小于x的元素个数
greater = n - bisect.bisect_right(a, x) # 严格大于x的个数
lesser = bisect.bisect_left(a, x) # 严格小于x的个数
return greater <= lesser
left, right = 0, 100000
ans = 0
while left <= right:
mid = (left + right) // 2
if check(mid):
ans = mid
right = mid - 1 # 尝试更小的(让中位数更"容易"达到)
else:
left = mid + 1
# 判断ans是否恰好是中位数
greater = n - bisect.bisect_right(a, ans)
lesser = bisect.bisect_left(a, ans)
t = 1 if greater == lesser else 0 # 是否需要调整
res = []
for i in arr:
if i >= ans:
res.append(0) # 不需要操作
else:
res.append(ans + t - i) # 需要操作到 ans+t
print(' '.join(map(str, res)))
推演验证
输入:n=5, arr=[1,2,3,4,5]
排序后:[1,2,3,4,5]
check(3): greater=5-3=2 (4,5), lesser=2 (1,2), 2<=2 True
check(4): greater=5-4=1 (5), lesser=3 (1,2,3), 1<=3 True
check(5): greater=0, lesser=4, 0<=4 True
但我们要找的是"使中位数最大的最小操作次数",所以需要重新理解题意...
实际上这题是:找最大的中位数,使得操作次数最少。
如果 ans=3, t=0: 1→3(2次), 2→3(1次), 3,4,5不变,总操作3次
如果 ans=4, t=1: 需要中位数为4,但greater=1,lesser=3不相等
最终输出每个元素变成的值,而非操作次数。
关键细节
| 坑点 | 说明 |
|---|---|
bisect_left vs bisect_right |
左边界找 <,右边界找 <= |
t 的调整 |
当 greater == lesser 时需要特殊处理 |
| 输出的是最终值 | 不是操作次数,是 ans+t |
例题 2:阶乘末尾零的个数(蓝桥杯 P2145)
| 项目 | 内容 |
|---|---|
| 链接 | https://www.lanqiao.cn/problems/2145/learning/ |
| 类型 | 二分答案 + 数论 |
| 核心 | 勒让德公式、二分找第K个 |
题目描述
求最小的正整数 n,使得 n! 末尾恰好有 k 个零。如果不存在,输出 -1。
输入格式
k
输出格式
一个整数,n 或 -1。
题解
数学知识 :n! 末尾零的个数 = n! 中因子5的个数(因为2比5多)。
勒让德公式 :count = n//5 + n//25 + n//25 + ...
二分找 n :check(n) 计算 n! 末尾零的个数。
python
k = int(input())
def check(x):
"""计算x!末尾有多少个零"""
cnt = 0
while x >= 5:
cnt += x // 5
x //= 5
return cnt
left, right = 1, 10**19 # 范围足够大
ans = 0
while left <= right:
mid = (left + right) // 2
if check(mid) >= k:
ans = mid
right = mid - 1 # 尝试更小的n
else:
left = mid + 1 # 零不够,需要更大的n
# 验证:必须恰好等于k,不能多
if check(ans) == k:
print(ans)
else:
print(-1)
推演验证
k=1:
check(4)=0, check(5)=1, check(6)=1...
二分找到ans=5, check(5)=1==1 → 输出5
k=2:
check(9)=1, check(10)=2, check(11)=2...
ans=10, check(10)=2==2 → 输出10
k=5:
check(24)=4, check(25)=6...
不存在恰好5个零的阶乘!
check(ans)=check(25)=6 != 5 → 输出-1
关键细节
| 坑点 | 说明 |
|---|---|
| 范围要够大 | 10**19 防止溢出,Python无压力 |
| 最后必须验证 | check(ans) == k,因为可能存在"跳跃" |
| 不存在的情况 | 当k=5时,24!有4个零,25!有6个零,没有5个零的 |
例题 3:最大子矩阵(蓝桥杯 P2147)
| 项目 | 内容 |
|---|---|
| 链接 | https://www.lanqiao.cn/problems/2147/learning/ |
| 类型 | 二分答案 + 滑动窗口 / 双指针 |
| 核心 | 枚举上下边界 + 滑动窗口找最宽列 |
题目描述
给定 n×m 矩阵,求满足"最大值-最小值 <= limit"的最大子矩阵面积。
输入格式
n m
矩阵
limit
输出格式
一个整数,最大面积。
题解
暴力做法 :枚举上下左右四边界,O(n²m²),超时。
优化:枚举上下边界,对每一列维护该区间内的最大最小值,转化为"一维滑动窗口"问题。
python
from collections import deque
n, m = map(int, input().split())
a = [list(map(int, input().split())) for _ in range(n)]
limit = int(input())
s = 0
# 枚举上下边界
for top in range(n):
# 初始化每列的最大最小值
col_min = a[top][:]
col_max = a[top][:]
for bottom in range(top, n):
# 更新每列的最大最小值
for j in range(m):
col_min[j] = min(col_min[j], a[bottom][j])
col_max[j] = max(col_max[j], a[bottom][j])
# 滑动窗口:找最长连续列,使得 max-min <= limit
left = 0
window_min = deque() # 维护窗口最小值(存下标)
window_max = deque() # 维护窗口最大值(存下标)
for right in range(m):
# 维护单调递增队列(最小值)
while window_min and col_min[window_min[-1]] >= col_min[right]:
window_min.pop()
window_min.append(right)
# 维护单调递减队列(最大值)
while window_max and col_max[window_max[-1]] <= col_max[right]:
window_max.pop()
window_max.append(right)
# 收缩左边界,直到满足条件
while left <= right:
cur_min = col_min[window_min[0]]
cur_max = col_max[window_max[0]]
if cur_max - cur_min <= limit:
break
left += 1
if window_min[0] < left:
window_min.popleft()
if window_max[0] < left:
window_max.popleft()
# 更新答案
area = (bottom - top + 1) * (right - left + 1)
s = max(s, area)
print(s)
关键细节
| 坑点 | 说明 |
|---|---|
| 滑动窗口维护的是列 | 不是元素,是上下边界确定的列区间 |
| 单调队列存下标 | 不是存值,方便判断元素是否滑出窗口 |
| 面积计算 | (bottom-top+1) * (right-left+1) |
例题 4:扫地机器人(蓝桥杯 P199)
| 项目 | 内容 |
|---|---|
| 链接 | https://www.lanqiao.cn/problems/199/learning/ |
| 类型 | 二分答案 + 贪心模拟 |
| 核心 | 模拟机器人清扫过程、判断时间是否足够 |
题目描述
n 个垃圾站排成一行,k 个机器人。第 i 个机器人在位置 a[i]。每个机器人可以左右清扫,速度为1(走一格1单位时间)。求清扫完所有垃圾的最小时间。
输入格式
n k
a[1]
a[2]
...
a[k]
输出格式
一个整数,最小时间。
题解
贪心策略:机器人按位置排序,每个机器人先往左清扫(如果左边还有垃圾),再往右清扫。
check(t) :给定时间 t,能否清扫完 [1, n]?
python
n, k = map(int, input().split())
a = [int(input()) for _ in range(k)]
a.sort()
def check(x):
"""给定时间x,能否清扫完所有垃圾"""
cnt = 0 # 当前清扫到的最右位置
for i in range(k):
cur = x # 当前机器人剩余时间
# 先往左清扫
if cnt < a[i] - 1:
# 需要走到左边未清扫的区域
need = 2 * (a[i] - 1 - cnt) # 走过去再回来
cur -= need
if cur < 0:
return False # 时间不够走到左边
# 往右清扫,计算能扫多远
# 从a[i]出发,剩余时间cur,可以向右走 cur//2 格(来回)
# 或者直接走到右边:如果不需要回来,可以走cur格
# 实际上机器人最后不需要回到起点,所以优化:
# 先左后右,最后停在右边,节省回来的时间
# 简化:假设先花时间去左边,然后剩下的时间往右
# 往右能扫到 a[i] + cur//2(如果之前去了左边)
# 或者 a[i] + cur(如果左边已经扫完)
if cnt >= a[i] - 1: # 左边已经扫完
cnt = max(cnt, a[i] + cur)
else:
cnt = max(cnt, a[i] + cur // 2)
return cnt >= n
left, right = 0, 2 * n
ans = 0
while left <= right:
mid = (left + right) // 2
if check(mid):
ans = mid
right = mid - 1
else:
left = mid + 1
print(ans)
关键细节
| 坑点 | 说明 |
|---|---|
| 先左后右的代价 | 去左边需要来回,代价 2*距离 |
| 最后不需要回来 | 往右清扫时,最后可以停在右边,只花单程 |
cnt 的含义 |
当前已经清扫到的最右位置 |
| 机器人排序 | 必须按位置排序,贪心从左到右处理 |
例题 5:区间覆盖(蓝桥杯 P111)
| 项目 | 内容 |
|---|---|
| 链接 | https://www.lanqiao.cn/problems/111/learning/ |
| 类型 | 二分答案 + 贪心 |
| 核心 | 浮点二分、区间覆盖、精度处理 |
题目描述
n 个区间 [u, v],每个区间可以扩展 mid(变为 [u-mid, v+mid])。求最小的 mid,使得扩展后的区间能覆盖 [0, 20000]。
输入格式
n
u1 v1
u2 v2
...
un vn
输出格式
最小的 mid,如果是整数输出整数,否则保留一位小数。
题解
浮点二分 :答案可能是 .5(因为输入是整数,扩展 mid 后边界可能是 x.5)。
check(mid):贪心选择能覆盖当前点的、右端点最远的区间。
python
n = int(input())
qujian = []
for i in range(n):
u, v = map(int, input().split())
qujian.append((u * 2, v * 2)) # 乘2避免浮点
qujian.sort(key=lambda x: (x[1], x[0]))
def check(mid):
cur = 0 # 当前覆盖到的最右位置(乘2后的坐标)
visited = [0] * n
while True:
flag = False
for i in range(n):
p = qujian[i]
if not visited[i] and p[0] - mid <= cur and p[1] + mid >= cur:
# 可以覆盖当前点,选择能延伸最远的
if p[0] + mid >= cur:
cur = max(cur, p[1] - p[0] + cur) # 从cur开始延伸
else:
cur = max(cur, p[1] + mid)
flag = True
visited[i] = 1
break # 贪心:选第一个能覆盖的
if not flag or cur >= 40000: # 20000*2
break
return cur >= 40000
# 二分答案(整数,最后除2)
left, right = 0, int(2e4)
while left < right:
mid = (left + right) // 2
if check(mid):
right = mid
else:
left = mid + 1
# 输出处理
if right % 2 == 0:
print(right // 2)
else:
print(right / 2)
关键细节
| 坑点 | 说明 |
|---|---|
| 乘2处理精度 | 避免浮点二分,用整数二分最后除2 |
| 贪心策略 | 选能覆盖当前点且延伸最远的区间 |
visited 数组 |
每个区间只能用一次 |
| 输出格式 | 整数或一位小数 |
📊 今日刷题总结
| 题号 | 考点 | 结合算法 | 难度 | 核心技巧 |
|---|---|---|---|---|
| P2143 | 中位数 | 二分 + 数学 | ⭐⭐⭐ | bisect统计大小关系 |
| P2145 | 阶乘零 | 二分 + 数论 | ⭐⭐⭐ | 勒让德公式、验证存在性 |
| P2147 | 最大子矩阵 | 枚举 + 滑动窗口 | ⭐⭐⭐⭐ | 单调队列维护最值 |
| P199 | 扫地机器人 | 二分 + 贪心模拟 | ⭐⭐⭐⭐ | 先左后右、时间分配 |
| P111 | 区间覆盖 | 二分 + 贪心 | ⭐⭐⭐⭐ | 乘2避浮点、覆盖延伸 |
🎯 二分答案进阶技巧
| 技巧 | 适用场景 | 例题 |
|---|---|---|
| 乘2/乘10避浮点 | 答案含0.5或小数 | P111 |
| 先验证存在性 | 可能无解 | P2145 |
| 贪心模拟 | 过程复杂,需要逐步验证 | P199 |
| 转化为判定问题 | 求最优值 | 所有二分答案 |
| 结合数据结构 | 需要快速统计 | P2143用bisect |
💡 今日心得
- 二分答案不是孤立算法 :必须结合
check()函数,而check()可能是贪心、模拟、数学公式 - 浮点精度用整数替代:P111乘2、P2145用大整数,避免浮点误差
- 验证最后答案 :P2145必须
check(ans)==k,防止跳跃导致无解 - 贪心策略要正确:P199先左后右,P111选延伸最远的区间,都要证明正确性
- 滑动窗口维护最值 :P2147用单调队列,将
O(m)优化到O(1)