二分搜索?莫非就是对于一个值单调递增的序列,给出(l,r),将要找的值与区间([l,r])的中点值(a[m])比较,大了(l)变成(m),小了(r)变成(m)呗?但是如果不细究,很容易发生off one错误。下面我们将要讨论该问题。
序列内没有重复数字
int Bsearch(int l, int r, int key, int *a)
{
while(l <= r)
{
int mid = (l + r) >> 1;
if (a[mid] == key)
return mid;
if (key < a[mid])
l = mid + 1;
else
r = mid - 1;
}
return -1;
}
因为没有重复数字,所以如果mid的值就是所求,返回mid就可以,否则既然mid的值无效了,范围缩小后的区间就不包括mid了。
LowerBound和UpperBound
int LowerBound(int l, int r, int key, int *a)
{
if (key > a[r])
return NO_ANS;
while(l < r)
{
int mid = (l + r) >> 1;
if (key <= a[mid])
r = mid;
else
l = mid + 1;
}
return l;
}
int UpperBound(int l, int r, int key, int *a)
{
if (key < a[l])
return NO_ANS;
while(l < r)
{
int mid = (l + r + 1) >> 1;
if (key >= a[mid])
l = mid;
else
r = mid - 1;
}
return l;
}
定义
LowerBound:给出(l,r,x),通过给出(p)的形式,求得一个区间([p,r]),使得任意一个下标(k),(kin [p,r]Leftrightarrow a_kgeq x)
UpperBound:给出(l,r,x),通过给出(p)的形式,求得一个区间([l,p]),使得任意一个下标(k),(kin [l,p]Leftrightarrow a_kleq x)。
(m=frac{l+r}{2})
左区间:([l,lfloor m
floor]),右区间:([lceil m
ceil,r])。
在LowerBound代码中,mid为(lfloor m
floor);在UpperBound代码中,mid为(lceil m
ceil)。
原理
看看两个代码中mid的定义,发现无论是UpperBound,还是LowerBound,其缩小区间时,要么缩小到左区间,要么缩小到右区间。
序列中存在(a_k=x)
这意味着(k)的值可能有多个。
(r-l+1)为偶数
这意味着(m
otin mathbb{Z})。如果(a_{lfloor m
floor},a_{lceil m
ceil})中存在不多于1个与(x)相等,这简单,若(xgeq a_{lceil m
ceil}),则转移到右区间;若(xleq a_{lfloor m
floor}),则转移到左区间。两个代码都满足这个要求。
如果(a_{lfloor m
floor},a_{lceil m
ceil})两个值都与(x)相等,则因为LowerBound要的(k)值最小,所以它要舍弃(a_{lceil m
ceil}),在左区间中寻找(kleqlfloor m
floor,a_k=x),故要求转移到左区间。同理UpperBound要求转移到右区间。
(r-l+1)为奇数
这意味着(minmathbb{Z})。若(a_m
eq x),则若(x<a_m),则转移到左区间,否则转移到右区间;
若(a_m=x),LowerBound要在左区间中寻找(kleqlfloor m
floor,a_k=x),故要求转移到左区间。同理UpperBound要求转移到右区间。
序列中不存在(a_k=x)
一开始是普通的二分,最后会遇到这种情况:(m otinmathbb{Z}),(a_{lfloor m floor}<x<a_{lceil m ceil})。如果是LowerBound,因为(x>a_{lfloor m floor}),LowerBound的定义要求所得区间内所有值都要大于(x),故要舍弃(a_{lfloor m floor}),故转移到右区间;UpperBound同理转移到了左区间。
我们看到mid的定义以及小于大于等于判断及其操作,都满足上述要求。