前言:二分法主要用于快速的查找某个数,当这段区间满足二段性时我们就可以使用这个方法来以O(log n)的时间复杂度来完成目的。二分的模板有好几个,但我这里就固定使用一个模板来解决问题了,二分的模板也不难难度主要体现在题目里奇奇怪怪的细节问题
1.二分的概念及其模板
当一个区间可以被某个条件分为左右两个区间时,我们就称之为具有二段性。假如我有一个非严格递增的区间,我要找到下标最小的数字3,这时我就可以先定义定义两个指针,一个为指向区间左端点的指针left,一个为指向区间右端点的指针right 。这时我们可以将这个区间分别两个区间,一个区间的数都是小于3的,另一个大于等于3:

此时我们定义一个中间变量mid:
cpp
int mid = (left + right + 1) / 2;//第一种
int mid = (left + right) / 2;//第二种
到底用哪种我会在介绍模板时有说明,这个mid的作用是更新区间,用于舍去不可能出现目标值的区间。
比如当mid落左区间,左区间因为都比3小所以不可能存在目标值所以这个时候我们就更新左指针left = mid + 1;当mid落在右区间就更新右指针right为 right = mid; 当程序不再满足left < right时就结束,此时左指针就指向了我们的结果3;
cpp
#include <iostream>
using namespace std;
int a[] = {0, 1, 1, 2, 3, 3, 4, 5};
int main()
{
int left = 1, right = 7;
while (left < right)
{
int mid = (left + right) / 2;
if (a[mid] < 3) left = mid + 1;
else right = mid;
}
cout << a[left] << endl;
return 0;
}
模板
这里你可以会有疑问,mid到底要不要加一,left < right 为什么不可以写成 left <= right 。这是因为在一些特定的情况下会出现死循环的结果,但是一个个慢慢的推又太慢了没必要,所以我这里就统一使用一个模板了:
循环的条件都统一为 left < right
当if / else 没有减1时,mid就不需要加1:
cpp
int l = 1, r = n;
while(l < r)
{
int mid = (l + r) / 2;
if(check(mid)) r = mid;
else l = mid + 1;
}
当if / else 出现了减1时,mid就需要加1:
cpp
int l = 1, r = n;
while(l < r)
{
int mid = (l + r + 1) / 2;
if(check(mid)) l = mid;
else r = mid - 1;
}
这个check就是二分的条件,只是上面的二分查找条件是大于小于某个数而已,使用比较的狭隘这点我后面介绍二分答案就可以明白了
其实在C++的SLT中本来就为我们准备好了现成的二分模板,但是只能有序的数组中使用,二分答案就用不了了,所以还是需要学习上面的二分模板。
这个模板包含在头文件:
cpp
#include <algorithm>
中,返回的是迭代器因此我们还需要对返回值解引用。这里我就简单的说一下这个模板的使用就不重复的写代码了:
cpp
lower_bound //返回⼤于等于 x 的最⼩元素,时间复杂度O(log N)
upper_bound //返回⼤于x的最⼩元素,时间复杂度O(log N)
2.二分查找经典例题
2.1牛可乐和魔法封印
这道题就是二分查找的经典应用了,我们需要二分两次分别把左、右区间找出来就可以了,这就是经典的套模板了,如何害怕mid溢出的话可以用:
cpp
mid = l + (r - l) / 2
或者直接开一个long long 比较省事
cpp
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
int n, q;
LL a[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
cin >> q;
while (q--)
{
LL k1, k2; cin >> k1 >> k2;
LL left = 1, right = n;
while (right > left)
{
LL mid = (left + right) / 2;
if (a[mid] >= k1) right = mid;
else left = mid + 1;
}
if (a[right] < k1)
{
cout << 0 << endl;
continue;
}
LL retright = right;
left = 1, right = n;
while (right > left)
{
LL mid = (left + right + 1) / 2;
if (a[mid] <= k2) left = mid;
else right = mid - 1;
}
if (a[left] > k2)
{
cout << 0 << endl;
continue;
}
cout << left - retright + 1 << endl;
}
return 0;
}
2.2在排序数组中查找元素的第⼀个和最后⼀个位置
同样的模板题,注意处理一下边界条件比如大小为空、找不到的情况下就可以了:
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int n = nums.size();
if (n == 0) return {-1, -1};
int left = 0, right = n - 1;
int mid = 0;
while (left < right)
{
mid = (left + right) / 2;
if (nums[mid] < target) left = mid + 1;
else right = mid;
}
if (nums[left] != target) return {-1, -1};
int retleft = left;
left = 0, right = n - 1;
while (left < right)
{
mid = (left + right + 1) / 2;
if (nums[mid] <= target) left = mid;
else right = mid - 1;
}
return {retleft, left};
}
};
2.3A-B 数对
这道题最好的写法其实是用哈希表来做,但是我们也可以转换一下思路尝试一下使用二分去解决这道问题。因为 A - B = C, 所以可以得出 A = B + C; 所以我们只要枚举一下A, 看看有多少个B满足条件就可以了:
cpp
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
LL n, c;
LL a[N];
int main()
{
cin >> n >> c;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + n + 1);
LL ret = 0;
for (int i = 1; i <= n; i++)
{
LL b = a[i] - c;
ret += upper_bound(a + 1, a + i, b) - lower_bound(a + 1, a + i, b);
}
cout << ret << endl;
return 0;
}
2.4烦恼的高考志愿
因为我们需要找出最接近高考分数的分数线,所以我们可以先找出大于等于高考分数的位置pos,那这个最接近的分数要么是pos要么就是pos - 1的位置,我们在这两个位置找一个分数线减去高考分数绝对值最小的就可以了:
cpp
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N];
int b[N];
int m, n;
int main()
{
cin >> m >> n;
for (int i = 1; i <= m; i++) scanf("%ld", &a[i]);
sort(a + 1, a + m + 1);
LL ret = 0;
int len = n;
while (len--)
{
int x; scanf("%d", &x);
LL left = 1, right = m;
LL mid;
while (left < right)
{
mid = (left + right + 1) / 2;
if (a[mid] <= x) left = mid;
else right = mid - 1;
}
int tem = left;
left = 1, right = m;
while (left < right)
{
mid = (left + right) / 2;
if (a[mid] >= x) right = mid;
else left = mid + 1;
}
int sz = min(abs(x - a[tem]), abs(x - a[left]));
ret += sz;
}
cout << ret << endl;
return 0;
}
3.二分答案
二分答案是在二分查找的基础上增加了对二分条件考察难度,这也是该类题目最难的地方因为确实是挺难想的。二分答案一般都是用来解决最小值最大、最大值最小的问题。因为题目的解集中有可能在又小变大的变化过程中出现二段性,下面我将以几个题目来举例
3.1木材加工
这次我们将切割的长度由小到大变成一个区间按,很明显根据题意长度越小切割的段数就越多,很明显存在个一个位置ret可以在满足段数的情况下切割出最大长度:

我们只需要另外写一个函数将段数转化成长度就可以了:
cpp
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL n, k;
LL a[N];
int chle(int x)
{
LL ret = 0;
for (int i = 1; i <= n; i++)
{
ret += a[i] / x;
}
return ret;
}
int main()
{
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
LL left = 0, right = 1e8;
while (left < right)
{
int mid = (left + right + 1) / 2;
if (chle(mid) >= k) left = mid;
else right = mid - 1;
}
cout << left << endl;
return 0;
}
3.2砍树
这道题与上道题几乎一摸一样我这里就直接贴代码了:
cpp
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
LL a[N];
LL m, n;
LL chie(LL 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];
LL left = 1, right = 2e9;
while (left < right)
{
LL mid = (left + right + 1) / 2;
if (chie(mid) >= m) left = mid;
else right = mid - 1;
}
cout << left << endl;
return 0;
}
完