目录
一:二分查找概念定义
二:整数二分查找的两个万能模板(借鉴试用多年,从未出错)
常见问题:为何mid有两种取值模板?
三:经典题目
问题一:
问题二(浮点数二分,但是比整数二分要简单):
问题三:
四:隆重介绍头文件中的lower_bound和upper_bound函数
五:拓展题型
山脉数组
六:习题练习(吃透这些题型即可彻底掌握二分)
二分查找解决的是单调函数上的查找问题。然后就有人问了,我遇到的二分查找都是在数组中找一个数,这个也是函数吗?
广义地来说,数组就是一些离散的点,所以它是一种离散函数。所以,数组元素的查找其实也是在函数中进行查找。如下图所示,代表的是一个五个元素的数组:
二分模板一共有两个,分别适用于不同情况。
算法思路:假设目标值在闭区间[l, r]中, 每次将区间长度缩小一半,当l = r时,我们就找到了目标值。
模板1(往左找答案):当我们将区间[l, r]划分成[l, mid]和[mid + 1, r]时,其更新操作是r = mid或者l = mid + 1;,计算mid时不需要加1。
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
模板2(往右找答案): 当我们将区间[l, r]划分成[l, mid - 1]和[mid, r]时,其更新操作是r = mid - 1或者l = mid;此时为了防止死循环,计算mid时需要加1。
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
答:我们拿第二个模板举例子,如果在第二个模板的情况下取mid=l+r>>1,在l+1=r且check函数成立的情况下,那么会运行l=mid这条语句,但是因为mid=l+r>>1,因此此时的l=mid=l,等于没变,所以就会陷入死循环,因此第二种模板的mid要上取整,也就是mid=l+r+1>>1
#include
using namespace std;
const int N = 100010;
int n, m;
int q[N];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
while (m -- )
{
int x;
scanf("%d", &x);
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r >> 1;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
if (q[l] != x) cout << "-1 -1" << endl;
else
{
cout << l << ' ';
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (q[mid] <= x) l = mid;
else r = mid - 1;
}
cout << l << endl;
}
}
return 0;
}
注:该题目十分经典,用到了两个二分的模板,思路并不难,注意输出的格式就行
法一(浮点数二分):
#include
using namespace std;
int main()
{
double x;
cin >> x;
double l = -100, r = 100;
while (r - l > 1e-8)
{
double mid = (l + r) / 2;
if (mid * mid * mid >= x) r = mid;
else l = mid;
}
printf("%.6lf\n", l);
return 0;
}
法二(c++函数crbt):
#include
using namespace std;
int main ( ) {
double n ;
cin >>n ;
printf ( "%lf" , cbrt ( n ) ) ; // cbrt 求三次方根
return 0 ;
}
注:浮点数二分的模板很简单,但是要注意精度的问题,一般精度eps取到1e-8就肯定可以AC;法二就是利用库函数了,sqrt是求二次方根,crbt是求三次方根,两者都是默认保留六位小数
#include
using namespace std;
const int N = 100010;
int n;
int q[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
int x;
scanf("%d", &x);
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r >> 1;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
printf("%d",l);
return 0;
}
注:返回其应该插入的位置,很明显是往左找答案,应该用模板1
在从小到大的排序数组中,
lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
举例:
#include
#include
using namespace std;
const int N = 100010;
int n, m;
int q[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
int x;
scanf("%d", &x);
int res1 = lower_bound(q,q+n,x)-q;//返回数组中第一个大于或等于被查数的值
int res2 = upper_bound(q,q+n,x)-q;//返回数组中第一个大于被查数的值
printf("%d %d\n",res1,res2);
return 0;
}
//输出1 3
private int findMountainTop(MountainArray mountainArr, int left, int right) {
while (left < right) {
int mid = left + right>>1;
if (mountainArr.get(mid) < mountainArr.get(mid + 1)) {
// 下一轮搜索区间 [mid + 1..right]
left = mid + 1;
} else {
// 下一轮搜索区间 [left..mid]
right = mid;
}
}
// left == right
return left;
}
注:这道题目的本质就是三个二分,上述我只给出最具有思考意义的二分,其他两段二分和模板一模一样背过就行。这个二分的check函数具有一定的含金量
当mid
序号 | 题目链接 | 难度系数 |
1 | 二分查找 | ★☆☆☆☆ |
2 | 猜数字大小 | ★☆☆☆☆ |
3 | 两数之和-输入有序数组 | ★★☆☆☆ |
4 | 搜索插入位置 | ★★☆☆☆ |
5 | 查找插入位置 | ★★☆☆☆ |
6 | 寻找比目标字母大的最小字母 | ★★☆☆☆ |
7 | 两数之和 | ★★☆☆☆ |
8 | 和为s的两个数字 | ★★☆☆☆ |
9 | 排序数组中两个数组之和 | ★★☆☆☆ |
10 | 按权重生成随机数 | ★★☆☆☆ |
11 | 按权重随机选择 | ★★☆☆☆ |
12 | 区域内查询数字的频率 | ★★★☆☆ |
13 | 采购方案 | ★★★☆☆ |
14 | 早餐组合 | ★★★☆☆ |
15 | 寻找峰值 | ★★★☆☆ |
16 | 最大连续1的个数III | ★★★☆☆ |
17 | 尽可能使字符串相等 | ★★★☆☆ |
18 | 制作m竖花所需要的最小天数 | ★★★☆☆ |
19 | 统计【优美子数组】 | ★★★★☆ |
20 | 摘水果 | ★★★★☆ |
21 | 山脉数组中查找目标值 | ★★★★☆ |