二分算法
实数二分不需要考虑是否补 1 的问题,因此相对而言更简单。
有单调性则一定可以二分,能二分却不一定具有单调性。没有单调性也可能可以二分。
整数二分思想
整数二分特别容易出现死循环。
-
确定一个区间,使得目标值(答案)一定在区间中。
-
找一个性质,这个性质具有二段性(95% 的二分都具有二段性):所谓的二段性就是区间内每个点都可以判断是否具有这个性质,并且利用这个性质,可以将一个区间划分为连续的两个部分,前半段区间满足性质,后半段不满足此性质,中间没有缺失的地方。 答案必须是二分的分界点。
-
分析中点 M 在该判断条件下是否成立,如果成立,考虑答案在哪个区间;如果不成立,考虑答案在哪个区间。(二分可以分为两大类,ans 是红色区域的右端点、ans 是绿色区域的左端点。)
-
如果更新方式写的是 R = M,则不用做任何特殊处理;如果更新方式写的是 L = M,则需要在计算 M 时额外加上 1,产生向上取整的效果,防止死循环。
以下模板基本可以解决所有二分问题。
第一类:ans 是红色区域的右端点
将 [L, R] 区间利用二段性分成 **[L, M - 1] 、 [M, R] **
if M 是红色,说明 ans 必然在 [M, R] 中:
else M 是绿色,说明 ans 必然在 [L, M - 1] 区间中。
根据上面的思想,有整数二分的第一个模板
。
int L = ,
R = ; // 确定 [L,R]区间
while(L < R) {
M = (L + R + 1) / 2; // 这里一定要额外+1,表示上取整。不补1,则为下取证,容易发生死循环
if M = 红 L = M; // [M, R]
else R = M - 1; // [L, M - 1]
}
记忆的小技巧:只要看 if 的判断条件是否有 L = M,如果有,则必须补上 + 1。
关于上述代码中的 M = (L + R + 1) / 2 说明:
一定要补上 1。
如果不补上 1 时,当 L = R - 1 时,即 R = L + 1,则 M = (L + R)/2 = (2*L + 1)/ 2 = L (C++ 默认下取整)。根据上述代码的步骤,当 M 在红色区域时,L = M = L,说明一次循环之后,L、M 都没有发生变化,这就导致了死循环。
如果补上 1 时,当 L = R - 1 时,即 R = L + 1,则 M = (L + R + 1) / 2 = (2*L + 2) / 2 = L + 1。根据上述代码的步骤,当 M 在红色区域时,L = M = L + 1,说明一次循环之后,L 发生了变化,这就不会发生死循环了。
C++ 上、下取整
C++ 默认是下取整,所以 (L + R) / 2 是 下取整,但是 (L + R + 1) / 2 的 下取整 就相当于 (L + R) / 2 的上取整 版本。
第二类:ans 是绿色区间的左端点
将 [L, R] 分成 [L, M] 和 [M + 1, R]
if M 是绿色,说明 ans 必然在 [L, M] 中
else 说明 ans 必然在 [M + 1, R] 中
整数二分的第二个模板
int L = ,
R = ; // 确定 [L,R]区间
while (L < R) {
M = (L + R) / 2;
if M = 绿色 R = M;
else L = M + 1;
}
只需要关注是否写了 R = M,如果写了,则一定不需要补 1。
关于上面代码为 M = (L + R) / 2 的说明
这次不会发生死循环。
当 L = R - 1 时,即 R = L + 1,M = (L + R) / 2 = (2*L + 1)/2 = L。此时,当 M 在 绿色区间时,R = L,说明一次循环 R 发生了变化,不会发生死循环。当 M 不在绿色区间时,L = M + 1 = L + 1,说明一次循环 L 发生了变化,不会发生死循环。
实数二分
实数二分有一个特点就是我们必然可以取到区间的中点,因为实数是稠密的。将区间划分成 [L, M] 与 [M, R]。
因为实数二分是稠密的,因此 while 循环的结束条件不能是 L < R,应该结束条件定义成区间长度小于某个值,一般取 1e-6 即可。但是,如果题目中的小数保留了 x 位,那么我们一般取 1e-(x+2) 区间长度才可。
if ans 在 [M, R] L = M
else if ans 在 [L, M] R = M
while(R - L > 1e-6) {
double M = (L + R) / 2;
if(...) L = M;
else R = M;
}