头痛的二分查找


二分手册(持续总结更新)

经过长时间的保守二分法调试的折磨(主要是边界条件的一些细节).

经常看着别人的代码与自己的代码明明是一样的,但是别人的直接AC,我的要么TLE,要么CE,要么SF……..

所以,经过长时间的血泪教训与总结,我开始渐渐摸清楚二分的方法了吧(Maybe?)

对于常用的二分模板来说

最应该考虑二分的目的与意义

二分:翻译成通俗易懂的语言来说,就是找到第一个满足XX条件的数据(前提是在一个有序的数组中)

主要有两种,一种是a[mid] >= target型的,一种是a[mid] <= target型的。

两种不同的表达方式对应着不同的left与right的部署。

while(left < right)
{
    int mid = left + right >> 1 ; 
    if(a[mid] >= target)
    {
        right = mid ; 
    }
    else
    {
        left = mid + 1 ; 
    }
}
//中文就是大于等于target的第一个数字
while(left < right)
{
    int mid = left + right + 1>> 1 ; 
    if(a[mid] > target)
    {
        right = mid-1 ; 
    }
    else
    {
        left = mid  ;
    }
}
//找到的是小于等于target的第一个数字 (只要是right = mid -1 ,要在mid的来源+1)

举一些例子:

旋转数组的最小数字


把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

输入一个升序的数组的一个旋转,输出旋转数组的最小元素。

例如数组 {3,4,5,1,2}为 {1,2,3,4,5}的一个旋转,该数组的最小值为 1。

数组可能包含重复项。

注意:数组内所含元素非负,若数组大小为 0,请返回 −1。

样例

输入:nums = [2, 2, 2, 0, 1]

输出:0

这一题是比较经典的利用二分来做的,找最小值的模板题目。

其实如果是比赛做到了,那其实直接sort一下,return nums[0] 好像就可以了……..(好吧,某种程度上说他显然是一道弱智题…….)

但是!!!但是!!!但是!!!

这道题可以锻炼一下我们的二分的熟练度,好吧,现在就用二分来分析一下这道题目,就用刚刚的二分模板。

class Solution {
public:
    int findMin(vector& nums) {
        int n = nums.size() ; 
        if(n == 0) return  -1 ; 
        
        nums.erase(unique(nums.begin() , nums.end()) , nums.end()) ;
        
        n = nums.size() ; 
        
        int left = 0  ; int right = n-1 ; //左闭右开
        
        if(nums[right] > nums[0]) return nums[0] ; 
        
        while(left < right)
        {
            int mid = left + right >> 1 ; 
            
            if(nums[mid] >= nums[0])
            {
                left = mid + 1 ; 
            }
            else
            {
                right = mid ; 
            }
        }
        return nums[left] ; 
    }
};

回顾一下代码:

写二分,重要的一下几点:

一、找到target,即你的对比值是什么。比如这一题他的对比值就是nums[0]。

二、找到你想要的值与target之间的关系,无非就是几种:

第一个小于等于target的数、第一个大于等于target的数,或者是把小于等于什么的改成小于而已…….

三、找对应的模板,背模板的时候注意几点即可。

我个人习惯是一般采用左闭右开的方式,来初始化的left和right。

然后找target与关系,找到之后呢,比如这一题,我要找的是小于nums[0]的第一个数字,那我就考虑left最终应该指向>=nums[0]的最后一个元素的位置再加一。

一般来说,我们最终的破除循环的条件为

while(left < right)
{
    ...........
}

所以最终状态其实是left == right的,那我们就可以反向进行考虑,最后一步变化的那必然是xxx = mid + 1/mid -1什么的,另外一个值其实再倒数几步的时候是不会变化的,已经锁定住我们要的值了,剩下的其实可以理解为等另外一个变量靠过来,然后破圈即可。所以我们主要考虑的对象其实是xxx = mid ; 的部分。如果是right = mid -1 ;什么的,要注意再确定mid的时候,就需要

mid = left + right + 1>> 1 ; 

要不然,血泪教训是

TLE……….死循环……

好的,之后如果有更加经典的例子,我再次会更新。

—————————————-ok 今天是-10-10更新————————————————————————

又发现了一个比较经典的例子

数的范围


给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。

如果数组中不存在该元素,则返回 -1

输入格式

第一行包含整数 n 和 q,表示数组长度和询问个数。

第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 k,表示一个询问元素。

输出格式

共 q行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1

数据范围

1≤n≤100000
1≤q≤10000
1≤k≤10000

输入样例:

6 3
1 2 2 3 3 4
3
4
5

输出样例:

3 4
5 5
-1 -1

ok,main函数的部分应该是相当简单了,主要的难点就是在于,如何返回两个值,满足起始与终点的要求。

#include 

using namespace std ;

const int N = 100010  ; 

int a[N] ; 

int n , m ;

int findlow(int *a , int num)
{
    int left = 0 ;
    int right = n ; 
    while(left < right)
    {
        int mid = left + right >> 1 ; 
        if(a[mid] >= num)
        {
            right = mid ; 
        }
        else
        {
            left = mid + 1 ; 
        }
    }
    return left ; 
}

int findhigh(int *a , int num)
{
    int left = 0 ;
    int right = n ; 
    while(left < right)
    {
        int mid = left + right >> 1 ; 
        if(a[mid] > num)
        {
            right = mid ; 
        }
        else
        {
            left = mid + 1 ; 
        }
    }
    return left - 1; 
}



int main()
{
    cin >> n >> m ; 
    
    for(int i = 0 ; i < n ;++i)
    {
        cin >> a[i] ; 
    }

    for(int i = 0 ; i < m ; ++ i)
    {
        int b ; 
        cin >> b ; 
        int low = findlow(a , b) ; 
        int high = findhigh(a , b) ; 
        if(low > high)
            low = high = -1 ; 
        cout << low << " " << high << endl ; 
    }
    return  0 ; 
}

运用之前的二分查找法的模板来说,

我们已经知道该题的target,就是目标值了。现在关系也明确了,就是查找>=target的第一个数字和<=target的最后一个数字。

我们知道二分查找模板的宗旨是返回第一个满足的值。那大于等于target的第一个值非常好找,小于等于target的最后一个值应该怎么找呢?

换个思路,那就是找>target的第一个值的位置再减去一咯!!!!!

int findlow(int *a , int num)
{
    int left = 0 ;  int right = n ; 
    while(left < right)
    {
        int mid = left + right >> 1 ; 
        if(a[mid] >= num)
        {
            right = mid  ;
        }
        else
        {
            left = mid + 1 ; 
        }
    }
    return left ; 
}
int findhigh(int *a , int num)
{
    int left = 0 ; int right = n ; 
    while(left < right)
    {
        int mid = left + right >> 1 ; 
        if(a[mid] > num)
        {
            right = mid ; 
        }
        else
        {
            left = mid + 1  ;
        }
    }
    return left - 1 ; 
}

文章作者: 罗林
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗林 !
  目录