二分
二分
该算法在数学中也有广泛的应用,在数学中,我们求函数零点时也会采取这种方式。
该算法思路非常简单:就是在知道答案的范围时,对答案进行夹逼,直到夹逼出一个固定值或者精度满足要求的数字为止。
大致过程
- 已知答案在 \([l,r]\) 的范围内;
- 每次取 \(mid=\frac{l+r} 2\);
- 对 \(mid\) 进行判定,判断 \(mid\) 是否满足条件;
- 如果 \(mid\) 满足条件,缩小区间为 \([mid,r]\),否则缩小区间为 \([l,mid]\);
- 回到 2 操作继续进行,直到找到答案或精度足够为止。
要求
- 在进行整数二分时,对于所二分的条件 check,长度为 \(n\) 的数组 \(a\) 中恰有一点 \(i\),使得 \([1,i]\) 都满足 check 函数,使得 \([i+1,n]\) 都不满足 check 函数。
- 小数二分求函数零点时,必须保证在所求范围内,函数需要满足确定的单调性。
时间复杂度
时间复杂度是一个典型的 \(O(\log n)\),常数也很小,效率非常高。
整数二分
适用情况
整数二分通常用于下列情况:求大于(等于)或小于(等于)一个数字的第一个(最后一个)数字、求整数答案、求整数(自然数)最优解,求第一个满足条件的情况或数字。
实现细节
整数二分时,我们的退出条件应为 \(l>r\) 或 \(l\geq r\),会因写法而异,而缩小区间时,我们需要使得 \(l:=mid+1\) 或 \(l:=mid\),\(r:=mid-1\) 或 \(r:=mid\),同样也因写法而异,如果没有选择合理的组合,二分就有可能死循环或者挂掉,而且最终是输出 \(l\) 还是 \(r\),\(l+1\) 还是 \(r-1\),\(l-1\) 还是 \(r+1\),都需要我们费脑子去想,如果没想对就会出错。
正如开头所说,二分的过程中细节非常多的,写错一点可能就很难调出,即使能够对于样例输出正确的答案,二分算法也不一定正确,我们需要找到一种稳定的方式,较为简单地实现整数二分。
在实践中,如果添加一个变量 \(res\) 来记录答案,就可以规避掉这个问题,具体写法如下:
int l,r,res=-1; // 将 l 和 r 设为二分范围
while(l<=r) {
int mid=l+r>>1;
if(check(mid)) res=mid,l=mid+1; // 根据所需选择 l=mid+1 还是 r=mid-1
else r=mid-1; // 同理,根据所需选择 l=mid+1 还是 r=mid-1
}
if(res==-1) ; // No Answer
else ; // 答案即为 res
这样,变量 \(res\) 记录的值就是我们的答案,规避掉了考虑输出 \(l\) 还是 \(r\) 的问题,所以,这种写法应该是整数二分中最为稳妥的。
小数二分
适用情况
小数二分通常用于下列情况:求函数零点、方程的解、求平均值或欧几里得距离等可能出现小数的答案。
实现
小数二分时,我们的退出条件应为 \(r-l<eps\),其中 \(eps\) 表示需要的精度,因题目要求而异,而缩小区间时,按需使 \(l:=mid\) 或 \(r:=mid\)。
不过,只要精度足够,输出 \(l\) 还是 \(r\) 其实都无伤大雅,所以,小数二分相对于整数二分而言就没有那么多烦人的细节,也没有太多多变的写法。
const double eps; // eps 表示所需精度
double l,r; // l 和 r 是二分区间
while(r-l>eps) {
double mid=(l+r)/2.;
if(check(mid)) l=mid; // 根据所需选择 l=mid 还是 r=mid
else r=mid; // 同理,根据所需选择 l=mid 还是 r=mid
}
// 此时,答案就是 l 或者 r 了,具体取哪个都无所谓

浙公网安备 33010602011771号