整数二分
二分思想
二分思想主要应用于找边界问题。
为了形象阐述二分思想以及它的应用,我打算举一个例子来说明。
假设有一个数列q,在该数列中存在一个区间,该区间左端元素的序号为l,右端元素的序号为r。
存在一个临界点p(边界),使得其右边的所有元素(包含p)都满足性质A,而其他元素都不满足性质A。
为了确定边界p的位置,我们就可以采用二分思想,具体做法如下:
- 取该区间的中间点mid:
mid=l+r2mid=\frac{l+r}2mid=2l+r - 检测序号为mid的元素是否满足性质A:
check(q[mid])={true mid元素满足性质Afalse mid元素不满足性质Acheck(q\lbrack mid\rbrack)=\left\{\begin{array}{l}true\;\;\;\;\;mid\mathrm{元素满足性质}A\\false\;\;\;mid\mathrm{元素不满足性质}A\end{array}\right.check(q[mid])={truemid元素满足性质Afalsemid元素不满足性质A
- 如果mid元素满足性质A,说明它在p点的右边(包含p),此时,可以缩小查找范围,缩小查找区间,变更后的查找区间应为:
[l, r=mid]\lbrack l,\;r=mid\rbrack[l,r=mid] - 如果mid元素不满足性质A,说明它在p点的左边(不包含p),此时,也可以缩小查找范围,缩小查找区间,变更后的查找区间应为:
[l=mid+1, r]\lbrack l=mid+1,\;r\rbrack[l=mid+1,r]
- 检查l是否等于r,如果相等,结果就为临界点p;如果不等,重复步骤一、二、三。
伪代码实现
int binary_search1(int q[], int l, int r) {
while (l < r) {
int mid = l + r >> 1;
if (check(q[mid])) r = mid;
else l = mid + 1;
}
return l;
}
左边界和右边界问题(重点重点!!!!)
前面我们讨论的是,假定临界点右边的元素(包含临界点)满足性质A,而其他元素不满足的情况。那如果我们考虑另一种情况,即:
考虑临界点左边的元素(包含临界点)满足性质A,而其他元素不满足。
此时,就出现了两种情况,也就是左边界问题和右边界问题。
我把第一种情况称为右边界问题,第二种情况称为左边界问题。
那么, 左边界问题和右边界问题的解决方法是否一致呢?
答:基本思想一致,但实现细节有差别(这一点差别很难发现,很容易造成程序死循环,形成BUG!!!!)
对于左边界问题,为了确定p的位置,具体做法如下:
- 取该区间的中间点mid:
mid=l+r+12mid=\frac{l+r+1}2mid=2l+r+1(为什么是(l+r+1)/2 ? 后面再解释) - 检测序号为mid的元素是否满足性质A:
check(q[mid])={true mid元素满足性质Afalse mid元素不满足性质Acheck(q\lbrack mid\rbrack)=\left\{\begin{array}{l}true\;\;\;\;\;mid\mathrm{元素满足性质}A\\false\;\;\;mid\mathrm{元素不满足性质}A\end{array}\right.check(q[mid])={truemid元素满足性质Afalsemid元素不满足性质A
- 如果mid元素满足性质A,说明它在p点的左边(包含p),此时,可以缩小查找范围,缩小查找区间,变更后的查找区间应为:
[l=mid,r]\lbrack l=mid,r\rbrack[l=mid,r] - 如果mid元素不满足性质A,说明它在p点的右边(不包含p),此时,也可以缩小查找范围,缩小查找区间,变更后的查找区间应为:
[l,r=mid−1]\lbrack l,r=mid-1\rbrack[l,r=mid−1]
- 检查l是否等于r,如果相等,结果就为临界点p;如果不等,重复步骤一、二、三。
伪代码实现
int binary_search2(int q[], int l, int r) {
while (l < r) {
int mid = l + r + 1 >> 1;
if (check(q[mid])) l = mid;
else r = mid - 1;
}
return l;
}
左边界中mid取值问题
在左边界问题中,为何mid=l+r+12mid=\frac{l+r+1}2mid=2l+r+1?
/*对于左边界问题,如果我们让mid = (l + r) / 2,就有可能出现下面的问题:
考虑下面程序在运行了几次循环之后,出现l = r - 1情况,此时mid = r - 1,
check(q[mid])也一定为true,那么l = mid = r - 1,于是就回到了最开始的情况,然后程序
就会陷入死循环。*/
int binary_search2(int q[], int l, int r) {
while (l < r) {
int mid = l + r >> 1;
if (check(q[mid])) l = mid;
else r = mid - 1;
}
return l;
}
//解决方法就是让mid = (l + r + 1) / 2,即可解决该问题。
int binary_search2(int q[], int l, int r) {
while (l < r) {
int mid = l + r + 1>> 1;
if (check(q[mid])) l = mid;
else r = mid - 1;
}
return l;
}
整数二分问题
给定一个按照升序排列的长度为 n 的整数数组p,以及 q 个查询。返回每一个待查找元素 k 的起始位置和终止位置(位置从 0 开始计数)。如果数组中不存在该元素,则返回 -1 -1。
解题思路:
采用二分思想,在该数组上定义一个性质:大于等于k,可以得到右边界点p0;然后在数组上再定义一个性质:小于等于k,可以得到左边界点p1。最后,p0就是待查找元素的终止位置,p1就是待查找元素的起始位置。
代码实现
#include<iostream>
using namespace std;
const int N=10000;
int p[N];
int n, q; //n为数组长度,q为查询次数。
int main()
{
scanf("%d%d", &n, &q);
for(int i=0; i < n; i++) scanf("%d", &p[i]); //为p[n]赋值,p[n]为升序整数数组。
while(q--){
int x;
scanf("%d", &x); //输入待查找的元素
int l = 0, r = n - 1; //初始化查找区间的左右端点序号
//对于右边界点问题,采用binary_search1()操作
while(l < r){
int mid = l + r >> 1;
if(p[mid] >= x) r = mid;
else l = mid + 1;
}
if(p[l] != x) cout << "-1 -1" << endl;
else{
cout << l << " ";
int l = 0, r = n - 1;
//对于左边界点问题,采用binary_search2()操作
while(l < r){
int mid = (l + r + 1) >> 1;
if(p[mid] <= x) l = mid;
else r = mid - 1;
}
cout << l << endl;
}
}
return 0;
}