二分算法,作为一种经典的算法策略,其核心精髓在于“二分”二字。顾名思义,它类似于数学中的二等分操作,即在算法中不断将待搜索的范围一分为二,以此高效缩小搜索空间,直至精确定位到目标元素或确认其不存在。。
1 何为二分?
二分算法是一种专为有序数据集(如数组或列表)设计的快速搜索算法。其核心思想在于,通过不断将搜索区间划分为更小的两个部分,并根据中间元素与目标值的比较结果,智能地选择保留包含目标值的那部分区间,从而逐步缩小搜索范围,直至找到目标元素的确切位置或确认其不存在于当前数据集中。
1.1 模拟二分过程:
想象一下,在一条从1到9的数轴上,我们急于找到某个特定目标点A(比如数字4)。为了高效地完成这一任务,我们可以采用二分搜索策略:
首先,我们确定搜索的初始范围,即数轴上的1至9区间。
然后,我们计算该区间的中点(mid=5),作为我们的初步参考点
接下来,我们比较中点与目标点A的大小。发现mid(即5)大于A(即4),这意味着目标点A位于中点的左侧。因此,我们果断地将搜索范围调整为左半部分,即从1到5。
在新的搜索范围内(1—5),我们再次执行上述过程,计算中点并比较。这次,我们会根据比较结果继续缩小搜索范围,直至找到目标点A的确切位置。
值得注意的是,在调整搜索范围时,我们总是移动那些使目标点可能落在搜索范围之外的端点(第一次我们移动右端点R,第二次移动左端点L)。这是因为,我们的目标是保持搜索范围始终包含目标点(如果它存在的话),而移动可能排除目标点的端点则能有效缩小搜索空间 。
2 整数二分
2.1 例题
我们从一个例题入手来分析整数二分。
问题描述
情人节到了,妮妮学姐的追求者实在太多了,她一共有 n 个追求者,第 i 个追求者赠送了 ai 朵颜色相同的花朵。每个追求者赠送的花朵颜色都不同。为了卖掉这些花并将所得款项捐赠给希望小学,妮妮学姐决定将 k 朵颜色不同的花朵打包成一个花束。请问她最多可以打包成多少个花束?
输入格式
第一行输入两个整数 n 和 k,分别表示妮妮学姐的追求者数量和打包需要的花朵数。
第二行输入 n 个整数 ai,表示每个追求者赠送的花朵数量。
数据范围保证:1≤𝑛,𝑘≤2×
,1≤ai≤1
。
输出格式
输出一个整数表示妮妮学姐最多可以打包成多少个花束。
样例输入
2 2 5 6
样例输出
5
2.2 分析
我们考虑总共需要的花朵数,假设答案为 x ,那么我们需要的花朵是则为 x×k 朵。对于第 i 位追求者送的花我们思考他能为答案提供多少有效的花朵,显然是 min(ai,x) 。这样我们就可以统计出每个人可提供的有效花朵的和是否达到了x×k,如果达到则说明我们可以凑出x 束花束,否则则不行。
2.3 代码
#include <iostream>
using namespace std;
const int N = 1e6 + 2;
#define ll long long
ll a[N];
int n, k;
bool cmp(ll mid)
{
ll ans = 0;
for (int i = 0; i < n; i++)
{
ans += (min(mid, a[i]));
}
return (ans / k) >= mid;
}
int main()
{
cin >> n >> k;
for (int i = 0; i < n; i++)
{
cin >> a[i];
}
ll l = 0, r = 1e15;
while (r > l)
{
ll mid = (l + r + 1) / 2;
if (cmp(mid))
l = mid;
else
r = mid - 1;
}
cout << l;
return 0;
}
我们来逐一分析一下代码
ll l = 0, r = 1e15;
while (r > l)
{
ll mid = (l + r + 1) / 2;
if (cmp(mid))
l = mid;
else
r = mid - 1;
}
这里我们先设定了初始搜索范围(0—),在右端点大于左端点的前提下,每次找到区间的中点mid,判断是否符合条件来移动左右区间,最后左右端点重合即找到最大值。
为什么要写成 mid = (l + r + 1) / 2
和 r = mid - 1
?
本质是整数除法时避免下取整带来的问题。
1. mid = (l + r + 1) / 2
- 目的:当处理整数除法时,为了避免在某些情况下丢失中间值,确保
mid
更偏向右侧。这在搜索区间为[l, r]
且需要找到第一个满足条件的mid
时特别有用。 - 解释:如果
l
和r
都是整数,那么(l + r) / 2
的结果将向下取整。如果l
和r
的差是奇数,这个操作将丢失中间的整数值。通过加1
,你可以确保在l
和r
差为奇数时,mid
会偏向r
,从而可能在循环中更快地找到满足条件的解或确保最终l
和r
相遇在正确的点上。
2. r = mid - 1
- 目的:缩小搜索范围,排除
mid
值。 - 解释:在二分查找中,当你发现
mid
不满足条件时(在这个例子中,cmp(mid)
返回false
),你需要更新搜索范围以排除mid
。将r
设置为mid - 1
可以确保下一次循环中不会再次选择mid
作为中点。
什么时候加一,什么时候减一呢?
bool cmp(ll mid)
{
ll ans = 0;
for (int i = 0; i < n; i++)
{
ans += (min(mid, a[i]));
}
return (ans / k) >= mid;
}
判断能不能打包成花束。
3 浮点数二分
浮点数二分与整数二分类似,只是浮点数二分不需要考虑下取整带来的加一减一的问题。
3.1 模板
const double eps = 1e-9; // 定义精度误差范围
bool cmp(double mid) {
// 判断条件
// 注意:这里需要根据题目要求来编写cmp函数
// 可能需要考虑浮点数的精度问题
}
int main() {
double l = 0, r = 1e9 + 3;
while (r - l > eps) {
double mid = (l + r) / 2.0;
if (cmp(mid)) {
l = mid;
} else {
r = mid;
}
}
// 由于浮点数的精度问题,最终l和r可能会非常接近但不相等
// 可以选择输出l、r或它们的平均值,具体取决于题目要求
return 0;
}
3.2 注意事项
- 循环条件:通常使用
while (r - l > eps)
作为循环条件,其中eps
是一个很小的正数,表示精度误差范围。 - 计算中点:直接使用
(l + r) / 2.0
(注意使用2.0
来确保进行浮点数除法)。 - 更新搜索范围:根据
cmp(mid)
的返回值来更新l
或r
。