二分法的特点:
二分法的应用领域:
// 在有序数组中查找目标值,返回索引,若不存在则返回-1
int binarySearch(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止整数溢出
if (nums[mid] == target) {
return mid; // 找到目标值
} else if (nums[mid] < target) {
left = mid + 1; // 目标在右半部分
} else {
right = mid - 1; // 目标在左半部分
}
}
return -1; // 目标值不存在
}
二分查找虽然思路简单,但实现时有几个容易出错的细节:
循环条件:while (left <= right) 还是 while (left < right)?
<= 时,退出循环后 left > right< 时,退出循环后 left == right中间值计算:mid = (left + right) / 2 可能导致整数溢出,更安全的写法是 mid = left + (right - left) / 2
边界更新:
left = mid + 1 和 right = mid - 1 是常见做法left = mid 或 right = midcheck(mid),用于检验当前解是否满足条件[left, right]check(mid)midcheck(mid)二分找答案的思维模型
想象一个数轴,其中:
或者反过来:
二分找答案就是要找到这个临界点,这要求问题具有单调性。
二分找答案适用于以下场景:
最大值最小化/最小值最大化问题
计数问题
资源分配问题
更多具体应用场景:
如何识别二分找答案的问题
当你遇到以下特征的问题时,可以考虑使用二分找答案
- 问题要求找到一个"最大/最小"值,使得某条件成立
- 问题具有单调性:随着变量增大/减小,条件满足情况呈现单调变化
- 可以设计一个判断函数,快速检验某个值是否满足条件
- 解空间范围明确,且可以用二分法高效搜索
关键是识别问题中的单调性,并将其转化为"寻找临界点"的形式。
寻找第一个满足条件的值(最小值):
// 在[left, right]范围内寻找第一个满足条件的值
int binarySearchFirstTrue(int left, int right) {
while (left < right) {
int mid = left + (right - left) / 2;
if (check(mid)) {
// mid满足条件,答案可能是mid或更小的值
right = mid;
} else {
// mid不满足条件,答案一定在mid右侧
left = mid + 1;
}
}
// 最终left==right,指向第一个满足条件的位置
return check(left) ? left : -1;
}
寻找最后一个满足条件的值(最大值):
// 在[left, right]范围内寻找最后一个满足条件的值
int binarySearchLastTrue(int left, int right) {
while (left < right) {
// 向上取整,避免死循环
int mid = left + (right - left + 1) / 2;
if (check(mid)) {
// mid满足条件,答案可能是mid或更大的值
left = mid;
} else {
// mid不满足条件,答案一定在mid左侧
right = mid - 1;
}
}
// 最终left==right,指向最后一个满足条件的位置
// 需要再次检查这个位置是否真的满足条件
return check(left) ? left : -1;
}
浮点数二分查找
当解空间是连续的实数域时,需要使用浮点数二分:
double binarySearchFloat(double left, double right, double epsilon) { while (right - left > epsilon) { // 精度控制 double mid = left + (right - left) / 2; if (check(mid)) { right = mid; } else { left = mid; } } return left; // 或right,此时两者非常接近 }
注意:
- 使用精度控制而非固定迭代次数
- 浮点数比较时要考虑精度问题
- 通常epsilon设为10^-6或10^-9
问题描述:
有
分析:
[1, max(L)]// 判断函数:给定长度length,是否能切割出至少K段
bool canCut(vector<int>& L, int K, int length) {
int count = 0; // 切割出的小木材段数
for (int wood : L) {
count += wood / length; // 每根木材可以切出的段数
}
return count >= K;
}
// 主函数:寻找最大可能长度
int woodCutting(vector<int>& L, int K) {
// 特殊情况处理
if (L.empty()) return 0;
// 确定解空间范围
int left = 1;
int right = *max_element(L.begin(), L.end());
// 如果无法切割出至少K段,直接返回0
long long sum = 0;
for (int wood : L) sum += wood;
if (sum < K) return 0;
// 二分查找
while (left < right) {
// 向上取整,避免死循环
int mid = left + (right - left + 1) / 2;
if (canCut(L, K, mid)) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
}
复杂度分析:
问题描述:
有
分析:
[1, (x[N-1] - x[0]) / (C-1)]distance,检查是否能放置// 判断函数:给定最小距离distance,是否能放置C头牛
bool canPlace(vector<int>& positions, int C, int distance) {
int count = 1; // 已放置的牛数量
int lastPos = positions[0]; // 上一头牛的位置
for (int i = 1; i < positions.size(); i++) {
if (positions[i] - lastPos >= distance) {
count++;
lastPos = positions[i];
if (count >= C) {
return true; // 已经放置了C头牛
}
}
}
return false;
}
// 主函数:寻找最大的最小距离
int angryCows(vector<int>& positions, int C) {
// 特殊情况处理
if (positions.size() < C) return -1;
// 确保位置已排序
sort(positions.begin(), positions.end());
// 确定解空间范围
int left = 1;
int right = positions.back() - positions.front();
// 二分查找
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (canPlace(positions, C, mid)) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
}
复杂度分析:
问题描述:
一条河上有
分析:
[1, positions[N-1] - positions[0]]distance,检查是否能通过移除不超过M个石头实现// 判断函数:给定最小距离distance,是否能通过移除不超过M个石头实现
bool canAchieve(vector<int>& positions, int M, int distance) {
int removed = 0; // 已移除的石头数量
int lastPos = positions[0]; // 上一个保留的石头位置
for (int i = 1; i < positions.size() - 1; i++) {
if (positions[i] - lastPos < distance) {
removed++;
} else {
lastPos = positions[i];
}
if (removed > M) {
return false;
}
}
return true;
}
// 主函数:寻找最大的最小距离
int stoneJump(vector<int>& positions, int M) {
// 特殊情况处理
if (positions.size() <= 2) return positions.back() - positions.front();
// 确保位置已排序
sort(positions.begin(), positions.end());
// 确定解空间范围
int left = 1;
int right = positions.back() - positions.front();
// 二分查找
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (canAchieve(positions, M, mid)) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
}
优化版本:
bool canAchieveOptimized(vector<int>& positions, int M, int distance) {
int count = 0; // 需要移除的石头数量
int curr = 0; // 当前石头的索引
int next = 1; // 下一个石头的索引
while (next < positions.size()) {
if (positions[next] - positions[curr] < distance) {
count++;
next++;
} else {
curr = next;
next++;
}
if (count > M) {
return false;
}
}
return true;
}
复杂度分析:
[POJ 2456] Aggressive cows
[HDU 1969] Pie
[LOJ 1201] 运输问题
[USACO 2018 Jan Silver] MooTube
[NOIP 2015] 跳石头
[CSES 1085] Array Division
[CSP-S 2021] 交通规划
[POJ 3258] River Hopscotch
[HDU 1551] Cable master
[NOIP 2012] 借教室
[USACO 2017 Open Platinum] COWBASIC
[IOI 2014] Game
二分找答案的核心要点:
二分找答案的优势:
进阶方向:
实践建议:
记住:二分找答案是一种思想,关键在于将问题转化为"寻找满足条件的临界点"的形式。

这个讲义使用了Awesome Marp主题的蓝色风格。 演讲者备注不会在演示模式中显示给观众,仅供演讲者参考。 每个关键页面都添加了备注,帮助演讲者更好地讲解内容。
封面页: - 欢迎学生,简要介绍本次课程的主题和目标 - 强调二分找答案是二分查找的进阶应用,是算法竞赛和面试中的常见题型 - 可以简单提及本课程将从基础概念到实际应用,全面讲解二分找答案的思想和技巧