[算法笔记]二分总结

一、什么时候使用二分?:

​ 大部分人接触二分应该二分查找算法,二分查找可以在一个有序区间中,找到指定的元素x的下标。

这里有两点:1、区间是有序的,即区间要满足一定条件。2、我们要找的数也有一定条件,二分查找要求找的数一定要等于x。

​ 我们再看两题:

AcWing.789.数的范围

题目给出的是按照升序排列的整数数组,要求一个元素k的起始终止位置,也就是第一个等于k的数和最后一个等于k的数。

LeeCode.852. 山脉数组的峰顶索引

题目给出一个山脉数组,即在峰顶元素左边升序排列,右边降序,可以算是半有序的状态。要找的就是峰顶元素。
可以看到,都满足我们提出的两个条件。当我们看到一个有一定条件分布的数组,要我们寻找一个较为特殊的值时,我们就可以考虑使用二分来做。

二、二分模板:

​ 大部分人最早接触二分算法应该都是二分查找算法,但是二分能做的远不止与此。二分算法的核心在于一个check(mid)函数,只要这个函数能够将给定的序列划分为两个区间,一个满足check(mid),一个不满足。就可以根据模板查找目标值。

​ 二分总体上可以分为两个模板,需要根据情况选择使用。

1、模板一:

int binary_search() {
    int l = 0, r = n - 1;
    while (l < r) {
        int mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    return l;
}

2、模板二:

int binary_search() {
    int l = 0, r = n - 1;
    while (l < r) {
        int mid = (l + r + 1) / 2;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

​ 两个模板的关键都在4~6行,即如何划分区间。模板一会将区间划为([l, mid]和[mid+1, r]),而模板二会将区间划为([l, mid-1]和[mid, r])这就是模板最重点的区别。

​ 模板上实际也是可以互换的

  • 两个模板最后返回时,都有(l ==r),且(l、r)都等于划分的第一个区间的右端点,即mid或mid - 1。

  • 模板二求mid时需要+1,这是为了防止死循环。

三、模板使用:

​ 下面画图说明两个模板的区别和应用:

​ 如果只想记住模板直接使用,有一个很好理解的方法。我们设xxx表示不满足check(mid)的序列元素,ooo表示满足,v表示我们需要的数。假设题目要求找到0到n之间的>5的第一个数,我们的序列类似这样。

0 1 2 3 4 5 6 7 8 9 10 ... n

x x x x x x v o o o o ... o

​ 可以看到,序列被划分成左边不满足、右边满足。我们要找的数就是第一个满足check(mid)的值,面对这样的划分区间,就可以套用模板一。

​ 假设要找 <= 5的最后一个数,我们的序列如下:

0 1 2 3 4 5 6 7 8 9 10 ... n

o o o o o v x x x x x ... x

序列左边满足、右边不满足check(mid)。v就是最后一个满足check(mid)的值,这样的划分区间就可以套用模板二。

​ 最后总结一下二分的思路:

  1. 首先把序列写出来,判断题目要求的是满足某一要求的第一个值而是最后一个值。
  2. 接着根据要求画出这样的划分情况分析。
  3. 如果形如x x x x v o o o,选择模板一,形如o o o v x x x,选择模板二。

三、思考:

​ 我们知道模板一模板二最后的l、r值是相同的,在模板一、二中分别等于mid、mid-1。两个模板也是可以相互转换的,第一个不满足check(mid)的值也就是最后一个满足check(mid)的值的下一个值。因此模板二的check(mid)改称与模板一的完全相反的判断函数,最后模板二返回l+1,就可以得到与模板一相同的答案。但是不建议这样使用。

​ 其次check(mid)函数选择还是有一定技巧的,如果不考虑像上面那样更改模板。那么check(mid)就决定了我们该使用哪个模板。

原文地址:https://www.cnblogs.com/enmac/p/13874227.html