二分的本质
如果有单调性的话一定可以二分,但是可以二分的题目不一定要有单调性
二分的本质是 寻找边界 (写不好易发生死循环),找到了边界就可以将区间二分(二分完保证答案在剩下的区间里)
给定一个区间,在区间上定义某种性质,左边区间满足该性质,右边区间不满足该性质,由此整个区间可以被一分为二,(注意:左右区间的边界并没有相交,因为是整数二分)
二分就可以寻找性质的边界(既可以二分绿色的点,也可以二分红色点,这就是两个模板的区别)
整数二分:
假设寻找二分红色的边界点
1、
初始时设置左右两个指针分别位于数组的左右两端,每次循环时计算中间值mid = l + r + 1>> 1
(至于mid 取值的原因后面解释),然后判断 check(mid) 的值(为实现二分查找,我们需要确保每次缩小区间时答案都落在区间内。这样一来,当最终 l == r 时,l 就是我们需要的答案)。
2、判断中间值是否满足红色区间的性质if (check(mid)),满足红色区间的性质if (check(mid))为真
若为true(即mid这一点满足红色区间性质),则mid一定在 左区间(因为如果mid在右边绿色区间,那么是不满足红色区间性质的,就会与true矛盾)
那么答案就在 [mid , r](这里困惑了好久,终于想明白了,为什么说答案一定在[mid , r],所谓答案是什么呢,就是能够满足红色性质区间的边界点,这个边界点再 + 1,就是绿色区间的边界,往[l, mid]中找肯定找不到,因为那边全是满足红色性质的,找不到二分红色的边界点) ,又因为mid 是有可能是 红色区间的边界点(我们找的就是 红色区间的边界点),所以 要包含mid,因此接下来令 l =mid 来缩小查找范围(因为我们要保证缩小后的区间仍然包含答案);更新方式就是将[l, r] 更新为[mid, r](使得l = mid即可)。
若为false,则mid一定取在 右边绿色区间(因为如果mid在左边红色色区间,那么是满足红色区间性质的,就会与false矛盾),所以答案一定在 [l, mid - 1](为什么不包含mid,因为mid一定不满足性质,所以答案的边界一定不包含mid,边界至少为mid - 1)更新方式就是将[l, r]更新为[l, mid - 1] 使得r = mid - 1即可
(为什么将mid设置为 l + r + 1 >> 1 不 + 1会死循环)
二分左区间的右边界点模板
int l = 0, r = n - 1;
while(l < r) {
int mid = l + r + 1 >> 1;
if(check[mid] ) l = mid;
else r = mid - 1;
}
假设二分绿色的边界点
1、
初始时设置左右两个指针分别位于数组的左右两端,每次循环时计算中间值mid = l + r >> 1
(至于mid 取值的原因后面解释),然后判断 check(mid) 的值(为实现二分查找,我们需要确保每次缩小区间时答案都落在区间内。这样一来,当最终 l == r 时,l 就是我们需要的答案)。
2、判断中间值是否满足绿色区间的性质if (check(mid)),满足红色区间的性质if (check(mid))为真
若为true(即mid这一点满足绿色区间性质),则mid一定在 右侧绿色区间(因为如果mid在左边红色区间,那么是不满足绿色区间性质的,就会与true矛盾)
那么答案就在 [l, mid](这里困惑了好久,终于想明白了,为什么说答案一定在[l , mid],所谓答案是什么呢,就是能够满足绿色性质区间的边界点,这个边界点再 - 1,就是红色区间的边界,往[mid + 1, r]中找肯定找不到,因为那边全是满足绿色性质的,找不到二分绿色的边界点) ,又因为mid 是有可能是 绿色区间的边界点(我们找的就是 绿色区间的边界点),所以 答案区间要包含mid,因此接下来令 r =mid 来缩小查找范围(因为我们要保证缩小后的区间仍然包含答案);更新方式就是将[l, r] 更新为[l, mid](使得r = mid即可)。
若为false,则mid一定取在 左边红色区间(因为如果mid在右边绿色色区间,那么是满足绿色区间性质的,就会与false矛盾),所以答案一定在 [mid + 1, r](为什么不包含mid,因为mid一定不满足性质,所以答案区间的边界一定不包含mid,边界至少为mid + 1)更新方式就是将[l, r]更新为[mid + 1, r] 使得l = mid + 1即可
二分右区间左边界点模板
int l = 0, r = n - 1;
while(l < r) {
int mid = l + r >> 1;
if(check[mid] ) r = mid;
else l = mid + 1;
}
二分问题代码思路
1、写mid = ?
2、写一个check (mid)函数
3、思考根据check (mid)函数的结果如何更新区间(模板一和模板二的区别,即找左边节点还是右边界点)
题目 求单调递增数数中 一个数的起始位置 和终止位置
核心二分代码
int k = Integer.parseInt(in.readLine());
int l = 0, r = n - 1;
while(l < r) {
int mid = l + r >> 1;
if(a[mid] >= k) r = mid;
else l = mid + 1;
}
if(a[l] != k) System.out.println("-1 -1");
else {
int left = l;//存储起始位置
l = 0;
r = n - 1;
while(l < r) {
int mid = l + r + 1 >> 1;
if(a[mid] <= k) l = mid;
else r = mid -1;
}
System.out.println(left + " " + l);
题目要求是 找一个数 在单调数组中的 起始位置和 终止位置,这个数也有可能在,也有可能不在。
首先说找一个数 的起始位置,可以根据起始位置的性质 来使用二分寻找
起始位置的性质,即在单调数组内,右边区间 >= x,而左边区间 < x,即右边区间一定满足 >= x的性质,而左边区间一定不满足 >= x的性质。
由此便可以 进行二分 来寻找边界点,找右区间的左边界点,便用到了二分模板
int l = 0, r = n - 1;
while(l < r) {
int mid = l + r + 1 >> 1;
if(check[mid] ) l = mid;
else r = mid - 1;
}
check(mid) 显然 就是 a[mid] >= x,当 l = r时便结束循环,找到了一个 a[l]
a[l]就是从左往右看 第一个 >= x的数(可能 >,也可能 >= ,
此时就进行一个判断
if(a[l] != k) System.out.println("-1 -1");
因为若 a[l] != x说明 他是 > x,后面的数也只能是 > x,而a[l]左边的数只能 < x,由此退出该数组 不存在 x,
else反之, a[l] = x, 说明 l = 就是x的起始位置,然后 就可以去找 终止位置了
还是那一套
可以根据终止位置的性质 来使用二分寻找
终止位置的性质,即在单调数组内,右边区间 > x,而左边区间 <=x,即左边区间一定满足 <= x的性质,而右边区间一定不满足 <= x的性质。
由此便可以 进行二分 来寻找边界点,找左区间的右边界点,便用到了二分模板
int l = 0, r = n - 1;
while(l < r) {
int mid = l + r >> 1;
if(check[mid] ) r = mid;
else l = mid + 1;
}
check(mid) 显然 就是 a[mid] <= x,当 l = r时便结束循环,找到了一个 a[l]
a[l]就是从右往左看 第一个 <= x的数(因为经过上面判断,已经保证了x一定在序列中,所以a[l] 一定 =x,a[l]即终止位置
至此 打印输出即可
代码答案
package algorithm_;
/**
* @author TJU第一炼丹师
* @since 2024-02-29 13:56:55
*/
import java.util.*;
public class 二分模板 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();//数组长度
int m = sc.nextInt();//询问次数
int q[] = new int[n];
for (int i = 0; i < n; i++) {//
q[i] = sc.nextInt();
}
while(m-- > 0){//循环 询问次数 次
int x = sc.nextInt();//输入 判断的数
int l = 0, r = n - 1;//设置初始边界
//寻找起始位置
while(l < r){//打破循环的条件时 l = r
int mid = l + r >> 1;//设置 中间值
if(q[mid] >= x){//因为是单增数组,找一个数的起始位置,所以找从左往右看第一个>=x的即可
r = mid;
}else{
l = mid + 1;
}
}
//判断数组是否存在这个数
if(q[l] != x)
System.out.println(-1 +" " + -1);
else{//存在开始找终止位置
int left = l;//存储起始位置
//重置边界开始寻找终止位置
l = 0;
r = n - 1;
while(l < r){
int mid = l + r + 1 >> 1;
if(q[mid] <= x){
l = mid;
}else{
r = mid - 1;
}
}
System.out.println(left + " " + l);
}
}
}
}