二分和三分总结与误区分析

来源:互联网 发布:qq业务乐园源码 编辑:程序博客网 时间:2024/05/21 09:40

今天学一个新算法的时候, 突然发现我以前的二分一直有点问题, 虽然以前的写的二分都能AC, 但还是有些问题, 如果题目给出的区间内没有解, 我的二分可能就会出问题. 加上更久之前发现自己三分写法的问题, 感觉自己能有些题目能AC真是运气好, 所以干脆把所有二分三分思路整理一下.


二分和三分总结与误区分析

  • 二分和三分总结与误区分析
    • 整数二分
      • 二分搜索
        • 写法一
        • 写法二
      • 二分求下界
        • 写法一
        • 写法二
    • 浮点数二分
    • 三分

整数二分

二分的坑点主要在整数的二分, 如果没有写好会出现死循环的问题.

二分的原理是利用区间内值有序的特点, 不断让可行区间减半, 最终可行区间长度减到1得到答案
要保证二分能得到正确答案并且不会死循环, 要保证两个条件:
1. 解一直在可行区间里
2. 每次判断后可行区间都会缩小(特别是左右端点相距为1的时候)

第一点很好保证, 第二点在可行区间长度大于2的时候不会出问题.
但是当可行区间左右端点相距为1的时候, 因为是整数, 所以中间值mid只能只能取得左端点或者右端点. 这时如果没有处理好, 就可能会陷入死循环.

所以重点要考虑的是当左右端点相距为1的时候, 执行判断后, 可行区间是否会缩小.
我们写二分的时候, 可以先保证第一点, 让解一直在可行区间里面, 然后再根据左右端点相距1的时候的情况, 调整mid到底是向上还是向上取整, 是+1还是-1

下面我们看几个常见的二分写法


二分搜索

写法一

在递增数组中寻找等于key值的位置
最初可行区间为[l, r)
循环退出条件为:
1. 找到了key, 直接return返回
2. 可行区间长度变为0, 无解, 退出并循环返回-1

int bsearch(int *a, int l, int r, int key){    int mid;    while(l<r)    {        mid = l + (r-l)/2;        if(a[mid] < key) l = mid+1;        else if(a[mid] == key) return mid;        else  r = mid;    }    return -1;}
  1. 当a[mid]偏小, 可行区间由[l, r)变为[mid+1, r), 因为数组递增, a[mid]< key, 所以解一定还在[mid+1, r)中
  2. 当a[mid]偏大, 由[l, r)变成[l, mid), 没问题

再来考虑当l=r-1时
1. 如果a[mid]偏小, 因为l=mid+1, 所以区间缩小了, 没问题;
2. a[mid]偏大, r = mid, 此时mid=l, 所以r也左移了, 区间缩小没问题

mid = l + (r-l);之所以写成这样而不是mid = (l+r)/2;是为了防止溢出, 并且具有更好的通用性(如果l和r是迭代器或者指针, 第一种就不行了, 因为迭代器或指针是不能相加的)

写法二

在递增数组中寻找等于key值的位置
最初可行区间为[l, r]
循环退出条件为:
1. 找到了key, 直接return返回
2. 可行区间长度变为0, 无解, 退出并循环返回-1

int bsearch(int *a, int l, int r, int key){    int mid;    while (l <= r)    {        mid = l + (r - l) / 2;        if (a[mid] < key) l = mid + 1;        else if (a[mid] == key) return mid;        else r = mid - 1;    }    return -1;}
  1. 分析同上, 偏小[l, r]->[mid+1, r], 偏大[l, r]->[l, mid-1], 判断之后区间缩小并且解还在区间内
  2. 但左右端点距离为1时, 如果偏小l=mid+1, 左端点右移, 区间缩小; 偏大r=mid-1, 右端点左移, 区间缩小;

二分求下界

区间[l, r)中, [l, x)都不满足某条件, [x, r)都满足条件, 求x
例如: 递增数列求第一个大于等于key的值([l, x)都满足< key, [x, r)都满足>=key, 求x)

写法一

递增数组求第一个大于等于key的值, 如果不存在, 返回最初区间的最后一个位置+1(r+1)
最初可行区间: [l, r]
退出循环条件, 可行区间长度变成1

int lowerBound(int *a, int l, int r, int key){    int mid;    while(l < r)    {        mid = l+(r-l)/2;        if(a[mid] < key) l = mid+1;        else r = mid;    }    if(a[l] == key) return l;    else return l+1;}

a[mid]< key, [l, r] -> [mid+1, r], 解还在区间内
a[mid]>=key, [mid, r], 因为可能等于key, 所以要保留mdi, 解还在区间内

当l=r-1时, mid=l, 如果a[mid] < key, l = mid+1, 左端点右移, 区间缩小;
如果a[mid]>=key, r = mid = l, 右端点左移, 区间缩小

最后还有一个判断, 是因为当解在最后一个位置时, 最后的区间变成了[r, r], 当无解的时候, 最后的区间也是[r, r], 所以要就一个判断看有没有解.

写法二

//同上, 最初可行区间为[l, r]int lowerBound(int *a, int l, int r, int key){    int mid;    while(l<=r)    {        mid = l + (r-l)/2;        if(a[mid] < key) l = mid+1;        else r = mid-1;    }    return l;}

和上一个大同小异, 但这个最后不用在判断是否存在, 因为再区间长度变成1的时候, 即l==r时, 还会在循环一次, 如果mid(mid= l)满足, l不变,如果mid不满足, l=mid+1. 相当于上一个程序循环外的判断放到了循环里面


浮点数二分

相比于整数的二分, 浮点数的二分好写多了, 因为没有取整的问题

有两种写法
1. 以循环次数为循环终止条件
2. 以精度位循环终止条件

for(int i=0; i<60; ++i) //i为循环次数, 60次循环可以让精度达到初始区间长度的1/1e18{    mid = (l+r)/2;    ...}while(r-l<eps)//其中eps是所需要的精度{    mid = (l+r)/2;    ...}

第二种如果eps太小, 由于浮点数的精度问题, 还是可能会陷入死循环, 所以推荐第一种

三分

三分可以用来求有极值点并且极值点两边一边递增一边递减的函数的极值点
同样有整数三分和浮点数三分
整数三分

//凸函数求极值点int lm, rm;while(l<r) {    lm = l+(r-l)/2;    rm = lm+(r-lm)/2;    if(a[lm] > a[rm]) r = rm;    else if(a[lm] == a[rm]) r=rm, l=lm;    else l = lm;}

特别注意, 当这个函数可能是单调函数时(极值点在端点), 三分算法无法到达端点位置, 所以要特判一下两个端点, 以前做题的时候被坑过
浮点数三分和二分一样可以用循环次数或精度来控制循环终止

原创粉丝点击