🌟 你用过微信"跳一跳"吗?每次按住屏幕的时间决定了小人的跳跃距离,有没有发现高手总能精准落在目标点?这背后藏着程序员的秘密武器——二分算法!今天让我们来揭开这个让搜索效率提升10000倍的魔法!
一、珍珠奶茶里的二分哲学
想象你要在500页的《奶茶配方大全》里找“黑糖珍珠”的做法。菜鸟会一页页翻(线性搜索),而老手会这样做:
-
翻到250页,发现是"奶盖制作"
-
往后翻到375页,看到"芋圆配方"
-
往前翻到312页...
这就是二分查找!每次排除一半错误答案,3步就找到目标,而菜鸟可能需要300步!
二、手把手教你写不会翻车的二分代码 🚗
二分算法的模板在网上找至少能找出三个以上。但是来到这里你算走运啦,你只需要掌握这一个并一直用下去就行了。
2.1 二分算法的模板
// 二分查找左端点
int l = 0, r = n;
while(l < r)
{
int mid = (l + r) >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
// 一定要注意检查答案的正确性
// 二分查找右端点
int l = 0, r = n;
while(l < r)
{
int mid = (l + r + 1) >> 1;
if(check(mid)) l = mid;
else r = mid - 1;
}
// 一定要注意检查答案的正确性
记忆方法:仅需记住一点,if/else 中出现 -1 时,求mid时 +1 即可。
2.2 小试牛刀
【解题】:在非递减顺序排列数组内查找目标值开始和结束的位置 -> 查找一堆目标值区间的左右端点 -> 二分算法。
问题一:万一找不到怎么办? 我们用二分算法本质是按照顺序往答案收敛,在原区间内找不到就意味着答案在区间之外(左右两侧),也就是说最后l < r,不满足跳出循环时 l 会在一侧,此时我们只需要对 l 位置检测一下是否为目标值即可。
🖥️code:
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans;
int n = nums.size();
if(n == 0) return {-1, -1};
int left = 0, right = n - 1;
while(left < right)
{
int mid = (left + right) >> 1;
if(nums[mid] >= target) right = mid;
else left = mid + 1;
}
if(nums[left] != target) return {-1, -1};
ans.push_back(left);
left = 0; right = n - 1;
while(left < right)
{
int mid = (left + right + 1) >> 1;
if(nums[mid] <= target) left = mid;
else right = mid - 1;
}
ans.push_back(left);
return ans;
}
};
2.3 趁热打铁

【解题】:一个数n的三次方根m,在它左侧 m^3 < n,它的右侧 m^3 > n,我们已经找出二段性,每次对mid判断时就可以舍弃一半的区间啦。
问题一:我们之前模板用的是整数二分,这题是浮点数二分? 这个可以当个模板来记忆,思想一模一样,只是多个精度。
🖥️code:
#include <iostream>
using namespace std;
const double eps = 1e-12; // 精度
double n;
int main()
{
cin >> n;
double l = -1e5, r = 1e5 + 10;
// l r 的差值小于精度时就可以认为找到了
while(l + esp < r)
{
double mid = (l + r) / 2;
if(mid * mid * mid > n) r = mid;
else l = mid;
}
printf("%.6lf\n", l);
return 0;
}
2.4 算法进阶


#include <iostream>
using namespace std;
const double eps = 1e-7;// 精度
double a, b, c, d;
double f(double x)
{
return a * x * x * x + b * x * x + c * x + d;
}
// 二分
double find(double l, double r)
{
while(r - eps > l)
{
double mid = (l + r) / 2;
if(f(l) * f(mid) < 0) r = mid;
else l = mid;
}
return l;
}
int main()
{
cin >> a >> b >> c >> d;
// 遍历-100 到 100 区间
for(double i = -100; i < 100; i++)
{
// 根恰好为整数
if(f(i) == 0) printf("%.2lf ", i);
// 根在i 和 i + 1之间
else if(f(i) * f(i + 1) < 0) printf("%.2lf ", find(i, i + 1));
}
return 0;
}
三、对二分算法的灵魂拷问🔥
3.1 为什么快?
每次都将问题的规模缩小一半。
-
1000个元素 → 10次搞定(2¹⁰=1024)
-
100万数据 → 20次解决!比线性搜索快5万倍!
超低的时间复杂度:O(log n)。
3.2 什么时候可以用二分算法?
当我们的解具有二段性的时候(下面习题),就可以使用二分算法得出答案:
-
待查找的区间一定有二段性!
-
根据待查找的区间中点位置,分析得出答案会出现在哪一侧。
-
接下来舍弃一半的待查找区间,转而在有答案的区间内继续使用二分算法查找结果。
3.3 二分算法的做题流程?
-
画图分析!确定使用左端点模板还是右端点模板,还是两者同时使用。
-
二分结果之后,不要忘记判断结果是否存在,二分问题细节众多,一定要分析全面。
四、二分法的七十二变 🎭
4.1 二分算法其他练习
4.1.1 牛可乐和魔法封印


🖥️code:
注意一定要开longlong 要不然求mid会溢出。
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N], n, q;
int solve(int x, int y)
{
LL l = 1, r = n;
while(l < r)
{
LL mid = (l + r) >> 1;
if(a[mid] >= x) r = mid;
else l = mid + 1;
}
if(a[l] < x) return 0;
int lowerbound = l;
l = 1, r = n;
while(l < r)
{
LL mid = (l + r + 1) >> 1;
if(a[mid] <= y) l = mid;
else r = mid - 1;
}
if(a[l] > y) return 0;
return l - lowerbound + 1;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
cin >> q;
while(q--)
{
int x, y; cin >> x >> y;
cout << solve(x, y) << endl;
}
return 0;
}
4.1.2 P1102 A-B 数对 - 洛谷

-
low_bound:传入要查询区间的左右迭代器(注意左闭右开, 如果是数组就是左右指针)以及要查询的值k, 然后返回 >= k的第一个位置。
-
upper_bound:传入要查询区间的左右迭代器(注意左闭右开, 如果是数组就是左右指针)以及要查询的值k, 然后返回 > k的第一个位置。
tip: 这两个函数只能用在有序序列中,我们二分的模板可以用在任意有二段性题目中。
🖥️code:
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
LL a[N];
LL n, c;
LL ans;
LL search(LL A)
{
auto it1 = lower_bound(a + 1, a + n + 1, A);
auto it2 = upper_bound(a + 1, a + n + 1, A);
return it2 - it1;
}
int main()
{
cin >> n >> c;
for(int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + n + 1);
// 遍历数组当b
for(int i = 1; i <= n; i++)
{
ans += search(a[i] + c);
}
cout << ans << endl;
return 0;
}
4.1.3 P1678 烦恼的高考志愿 - 洛谷

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10, INF = 1e7 + 10;
int n, m;
int a[N];
long long ans;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a[i];
// 左右护法
a[0] = a[n + 1] = INF;
sort(a + 1, a + n + 1);
while(m--)
{
int x; cin >> x;
auto it1 = lower_bound(a + 1, a + n + 1, x);
auto it2 = it1 - 1;
// cout << *it1 << " " << *it2 << endl;
ans += min(abs(*it1 - x), abs(*it2 - x));
}
cout << ans << endl;
return 0;
}
4.2 二分答案
二分答案可以处理最大值最小,以及最小值最大的问题。如果解空间在从小到大的变化过程中,判断答案的结果出现二段性,此时我们就可以二分这个解空间,通过判断找出最优解。
大家学习数学一定对应用题不陌生,没错二分答案就是二分算法的应用题,在这种题要善于找到其中隐藏的二段性。
4.2.1 P2440 木材加工 - 洛谷


#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int n, aim;
int calc(int x)
{
int ret = 0;
for(int i = 1; i <= n; i++) ret += a[i] / x;
return ret;
}
int main()
{
int maxlen = 0;
cin >> n >> aim;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
maxlen = max(maxlen, a[i]);
}
int l = 0, r = maxlen;
while(l < r)
{
int mid = (l + r + 1) >> 1;
int k = calc(mid);
if(k >=aim) l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
4.2.2 P1873 [COCI 2011/2012 #5] EKO / 砍树 - 洛谷

-
H越高,len越小。
-
H越低,len越大。
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
int n, m, maxlen;
int a[N];
LL calc(int x)
{
LL ret = 0;
for(int i = 1; i <= n; i++)
if(a[i] > x)
ret += a[i] - x;
return ret;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
maxlen = max(maxlen, a[i]);
}
int l = 0, r = maxlen;
while(l < r)
{
int mid = (l + r + 1) >> 1;
LL h = calc(mid);
if(h >= m) l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
4.2.3 P2678 [NOIP 2015 提高组] 跳石头 - 洛谷

-
len越长,n越多。
-
len越短,n越少。
于是在整个解空间中:
-
len <= ret 时,n <= M(给定的移动石头数量):当每次跳跃的最短距离 <= 最终结果,需要移动的石头数量少于或等于M。if(n <= M) l = mid;
-
len > ret 时,n > M(给定的移动石头数量):当每次跳跃的最短距离 > 最终结果,需要移动的石头数量多于M。if(n >M) r = mid - 1;
问题一:如何求在给定最短距离len下的移动石头次数n? 滑动窗口或前后指针法:两个指针i,j,从前往后扫描过程中第一次发现a[j] - a[i] >= len时,把j + 1 和 i - 1之间的石头搬走即可。
细节一:本题的数据最后一个位置是单独给的,处理时要n(这里表示数据个数)++,然后把最后一个也放入数组。
🖥️code:
#include <iostream>
using namespace std;
const int N = 5e4 + 10;
int a[N];
int len, n, k, m;
int calc(int x)
{
int ret = 0;
for(int i = 0; i <= n; i++)
{
int j = i + 1;
while(j <= n && a[j] - a[i] < x) j++;
ret += j - i - 1;
i = j - 1;
}
return ret;
}
int main()
{
cin >> len >> n >> k;
for(int i = 1; i <= n; i++) cin >> a[i];
a[++n] = len;
int l = 1, r = len;
while(l < r)
{
int mid = (l + r + 1) >> 1;
int t = calc(mid);
if(t <= k) l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
完。