算法_二分

二分的本质

如果有单调性的话一定可以二分,但是可以二分的题目不一定要有单调性

二分的本质是 寻找边界  (写不好易发生死循环),找到了边界就可以将区间二分(二分完保证答案在剩下的区间里)

给定一个区间,在区间上定义某种性质,左边区间满足该性质,右边区间不满足该性质,由此整个区间可以被一分为二,(注意:左右区间的边界并没有相交,因为是整数二分)

二分就可以寻找性质的边界既可以二分绿色的点,也可以二分红色点,这就是两个模板的区别

整数二分:
假设寻找二分红色的边界点

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);
            }

        }
    }
}

  • 11
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值