二分查找模板

  看了y总的二分,发现与我之前认识的二分完全不同。我之前学的二分查找是最简单的版本,就是在一个排好序的序列里找一个给定的数。而y总讲的二分更多考虑到了边界,就是通过二分找到满足某一条件的边界。现在终于明白为什么说二分的代码很恶心了。

 

整数二分

  首先要知道二分的本质并不是单调有序,也就是说不一定要满足单调有序才能够使用二分,不满足单调有序也是可以使用二分的。只要区间的一部分满足某一个条件,另一部分不满足这个条件,就可以使用二分,如下图(两个部分的边界是不重合的):

  假设区间绿色的部分是满足条件,而红色部分是不满足条件的,我们二分的结果是满足或不满足条件的边界,也就是箭头所指向的那个点。根据二分的不同结果我们有两种不同的模板。

  首先我们考虑找到满足绿色部分的边界,也就是第2个箭头的位置,取mid = left + right >> 1。有两种情况:

  • 如果mid位置的元素在绿色的部分,如下图:

  也就是mid位置的元素可能在满足条件的区域内部或是就在满足区域的边界(也就是我们要找的边界,二分的结果),说明我们要找的答案在mid的左边的部分(包含mid所在的那个元素),所以就要更新right = mid(注意,不是mid - 1),区间被划分为[left, mid]。

  • 如果mid的位置在红色的部分(不满足条件的部分),如下图:

  说明我们要找的答案在mid的右边的部分(不包含mid所在的那个元素),无论此时mid的位置是不是红色部分的边界,只要mid在红色的部分,就更新left = mid + 1,区间被划分为[mid + 1, right]。

  下面是这种二分的模板代码:

1 // 区间[left, right]被划分成[left, mid]和[mid + 1, right]时使用
2 int bsearch_2(int left, int right) {
3     while (left < right) {
4         int mid = left + right >> 1;
5         if (check(mid)) right = mid;    // check()判断mid是否满足性质
6         else left = mid + 1;
7     }
8     return left;
9 }

  接下来,我们考虑满足红色部分的边界,也就是第1个箭头的位置,取mid = left + right + 1 >> 1(注意这里要+1,原因就是防止出现边界问题)。有两种情况:

  • 如果mid位置的元素在红色的部分,如下图:

  也就是mid位置的元素可能在满足条件的区域内部或是就在满足区域的边界(也就是我们要找的边界,二分的结果),说明我们要找的答案在mid的右边的部分(包含mid所在的那个元素),所以就要更新left = mid(注意,不是mid + 1),区间被划分为[mid, right]。

  • 如果mid的位置在绿色的部分(不满足条件的部分),如下图:

  说明我们要找的答案在mid的左边的部分(不包含mid所在的那个元素),无论此时mid的位置是不是绿色部分的边界,只要mid在绿色的部分,就更新right = mid - 1,区间被划分为[left, mid - 1]。

  下面是这种二分的模板代码:

1 // 区间[left, right]被划分成[left, mid - 1]和[mid, right]时使用
2 int bsearch_2(int left, int right) {
3     while (left < right) {
4         int mid = left + right + 1 >> 1;    // 注意这里要+1 
5         if (check(mid)) left = mid;       // check()判断mid是否满足性质
6         else right = mid - 1;
7     }
8     return left;
9 }

  至于mid的取值什么时候要+1,可以这样记:如果该二分满足条件时是left = mid,就要+1;否则是right = mid就不需要+1。

  给个模板题:

789. 数的范围

给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。

如果数组中不存在该元素,则返回 -1 -1 

输入格式

第一行包含整数 n 和 q,表示数组长度和询问个数。

第二行包含 n 个整数(均在 1~10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 k,表示一个询问元素。

输出格式

共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1 -1 

数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000

输入样例:

6 3
1 2 2 3 3 4
3
4
5

输出样例:

3 4
5 5
-1 -1

  AC代码:

 1 #include <cstdio>
 2 using namespace std;
 3 
 4 int main() {
 5     int n, m;
 6     scanf("%d %d", &n, &m);
 7     int a[n];
 8     for (int i = 0; i < n; i++) {
 9         scanf("%d", a + i);
10     }
11     
12     while (m--) {
13         int val;
14         scanf("%d", &val);
15         int left = 0, right =n - 1;
16         while (left < right) {
17             int mid = left + right >> 1;
18             if (a[mid] >= val) right = mid;
19             else left = mid + 1;
20         }
21         
22         if (a[left] != val) {
23             printf("-1 -1\n");
24         }
25         else {
26             printf("%d ", left);
27             
28             left = 0, right = n - 1;
29             while (left < right) {
30                 int mid = left + right + 1 >> 1;
31                 if (a[mid] <= val) left = mid;
32                 else right = mid - 1;
33             }
34             
35             printf("%d\n", left);
36         }
37     }
38     
39     return 0;
40 }

 

浮点数二分

  浮点数二分就不想整形二分那样这么多的边界问题,思想与整数二分相似,更为简单。

  只不过就是把二分循环的条件 while(left < right) ,改为 while(right - left > eps) 。

  直接给出模板吧:

 1 const double eps = 1e-6;    // eps 表示精度,取决于题目对精度的要求
 2 
 3 double bsearch(double left, double right) {
 4     while (right - left > eps) {
 5         double mid = (left + right) / 2;
 6         if (check(mid)) right = mid;
 7         else left = mid;
 8     }
 9     return left;
10 }

  这里有个经验,就是如果题目要求保留m为小数,那么就取 double eps = 1e-(m + 2) ,比如如果要保留6位小数,那么eps = 1e-8。

  相应的模板题:

790. 数的三次方根

给定一个浮点数 n,求它的三次方根。

输入格式

共一行,包含一个浮点数 n。

输出格式

共一行,包含一个浮点数,表示问题的解。

注意,结果保留 6 位小数。

数据范围

-10000≤n≤10000

输入样例:

1000.00

输出样例:

10.000000

  AC代码:

 1 #include <cstdio>
 2 using namespace std;
 3 
 4 int main() {
 5     double n;
 6     scanf("%lf", &n);
 7     double left = -10000, right = 10000;
 8     while (right - left > 1e-8) {
 9         double mid = (left + right) / 2;
10         if (mid * mid * mid >= n) right = mid;
11         else left = mid;
12     }
13     printf("%.6f", left);
14     
15     return 0;
16 }

 

参考资料

  常用代码模板1——基础算法:https://www.acwing.com/blog/content/277/

posted @ 2021-07-21 17:21  onlyblues  阅读(460)  评论(0编辑  收藏  举报
Web Analytics