3.二分算法

3.二分算法

核心本质:找 “边界”,而非 “单调性”

  • 误区:认为 “只有有序数组才能二分”,实际只要区间能划分为 “满足性质” 和 “不满足性质” 两部分,即可二分;

  • 核心:每次缩小一半区间,且保证 “答案始终在区间内”,直到区间长度为 1(整数)或足够小(浮点数)。

3.1 整数二分:边界处理是难点

\[\begin{aligned} & \text{mid} = \frac{l+r+1}{2} \\ & if(check(mid)) = \begin{cases}true; & [mid,r],l=mid \\false; & [l,mid-1],r=mid-1\end{cases} \end{aligned} \]

\[\begin{aligned} & \text{mid} = \frac{l+r}{2} \\ & if(check(mid)) = \begin{cases}true; & [l,mid],r=mid \\false; & [mid+1,r],r=mid+1\end{cases} \end{aligned} \]

1.两种模板:对应两种边界场景​

  • 场景 1:找 “满足性质的左边界”(如 “第一个≥x 的数”);
  • 场景 2:找 “满足性质的右边界”(如 “最后一个≤x 的数”);
  • 关键区别:计算mid时是否加 1(避免死循环)。

2.模板 1:找左边界(第一个满足 “q [mid] ≥ x” 的数)​

  • 适用场景:求 “起始位置”(如数组[1,2,2,3,3,4]中,3 的起始下标 3)。

  • 代码模板(C++):

    bool check(int x) {/* ... */ } // 检查x是否满足某种性质
    // 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
    int bsearch(int l, int r)
    {
        while (l < r)
        {
            int mid = l + r + 1 >> 1;//如果下方else后面是l则这里加1
            if (check(mid)) l = mid;
            else r = mid - 1;//左加右减
        }
        return l;
    }
    

    解释:如果mid在红色区域,那么应该满足X≥mid(抽象简化),X应该在mid和r之间取值。否则X < mid,那么X应该在l和mid-1之间取值

    计算中间索引midint mid = l + r + 1 >> 1;,这里l + r + 1 >> 1是将l + r + 1的结果右移一位,相当于除以 2 并向下取整。当check(mid)true时,说明满足条件的值在mid及其右边的区间,所以更新l = mid;当check(mid)false时,说明满足条件的值在mid左边的区间,所以更新r = mid - 1。这部分逻辑在二分查找中对于将区间划分为[l, mid - 1][mid, r]这种划分方式是合理的。然而,函数最后return 1;是有问题的,正常二分查找应该返回找到的满足条件的索引值或者在未找到时返回特定标识(如 -1 等),直接返回 1 不能正确表示查找结果。

3.模板 2:找右边界(最后一个满足 “q [mid] ≤ X” 的数)​

  • 适用场景:求 “终止位置”(如数组[1,2,2,3,3,4]中,3 的终止下标 4)。

  • 代码模板(C++):

    // 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
    int bsearch(int l, int r)
    {
        while (l < r)
        {
            int mid = l + r >> 1;
            if (check(mid)) r = mid; // check()判断mid是否满足性质
            else l = mid + 1;//左加右减
        }
        return l;
    }
    

    解释:如果mid在绿色区域,那么应该满足X ≤ mid,X应该在l和mid之间取值。否则X > mid,那么X应该在mid+1和r之间取值。

    计算中间索引mid:int mid = l + r >> 1;,同样是计算中间索引,将l + r右移一位得到中间值。当check(mid)为true时,更新r = mid,意味着满足条件的值在mid及其左边的区间;当check(mid)为false时,更新l = mid + 1,意味着满足条件的值在mid右边的区间。这种区间划分和更新方式对于将区间划分为[l, mid]和[mid + 1, r]的二分查找逻辑是合理的,但由于循环条件错误,整个函数无法正确执行二分查找。同样,最后return 1;也不能正确表示查找结果。

4.示例:找数组中 x的范围([1,2,2,3,3,4],x=3)​

  • 找左边界:

    • 初始L=0,R=5,mid=2,q[2]=2 L=3`;
    • L=3,R=5,mid=4,q[4]=3 ≥3 → R=4;
    • L=3,R=4,mid=3,q[3]=3 ≥3 → R=3;
    • L=R=3,q[3]=3 → 返回 3(起始下标)。
  • 找右边界:

    • 初始L=0,R=5,mid=3((0+5+1)/2=3),q[3]=3 ≤3 → L=3;
    • L=3,R=5,mid=4((3+5+1)/2=4),q[4]=3 ≤3 → L=4;
    • L=4,R=5,mid=5((4+5+1)/2=5),q[5]=4 >3 → R=4;
    • L=R=4,q[4]=3 → 返回 4(终止下标)。

5.关键注意点(避坑!)​

  • 为什么模板 2 要加 1?

若L=R-1(如L=3,R=4),mid=(3+4)/2=3,若q[3] ≤x,则L=3,区间仍为[3,4],死循环;加 1 后mid=4,可正常收缩。

  • 无解处理:二分一定能找到 “边界”,但需判断边界值是否等于x(如找 x=6,左边界返回 5,q[5]=4≠6,故无解);

  • 应用扩展:不仅用于 “找值的范围”,还可用于 “最大值最小化”(如 “最小的最大值” 问题)、“最小值最大化”(如 “最大的最小值” 问题)。

3.2 浮点数二分:无边界烦恼,仅需控制精度

  1. 核心优势:无需处理整数整除的边界问题,区间长度可无限缩小。​
  2. 适用场景:求方程的解(如开平方、求立方根)。​
  3. 模板 1:按精度终止(推荐,与整数二分逻辑一致)​
  • 示例:求浮点数 X 的平方根(如 X=2,结果≈1.41421356)。

  • 代码模板(C++):

bool check(double x) {/* ... */ } // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
    const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}
  1. 模板 2:固定循环次数(简单粗暴)​
  • 原理:循环 100 次,区间长度缩小为初始的 1/(2100),精度足够(2100≈1e30,远小于 1e-8)。

  • 代码模板(C++):

bool check(double x) {/* ... */ } // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
    for (int i = 0; i ; i++) 
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}
  1. 关键注意点​
  • 精度选择:题目要求保留 k 位小数,精度需设为 1e-(k+2)(如保留 4 位小数,用 1e-6),避免四舍五入误差;

  • 初始区间:根据 X 的大小调整(如 X=0.01,初始区间设为 [0,0.1],而非 [0,0.01],避免收敛慢);

  • 应用扩展:可用于求任意单调函数的零点(如f(x)=x³-4,求 f (x)=0 的解)。

3.3 总结与扩展

  1. 算法对比表​
算法 思想 时间复杂度 空间复杂度 稳定性 核心应用场景
快速排序 分治(先分区) O(nlogn) O(logn) 不稳定 一般数组排序、TopK 问题
归并排序 分治(先排序) O(nlogn) O(n) 稳定 有序数组合并、逆序对统计
整数二分 边界查找 O(logn) O(1) - 找值范围、最值优化问题
浮点数二分 边界查找 O(logn) O(1) - 方程求解、开方 / 开立方
  1. 扩展知识点​
  • 逆序对统计:用归并排序实现(合并时统计左数组比右数组大的元素个数,时间复杂度 O (nlogn));

  • 快排优化:随机选择分界点 + 三数取中(避免极端情况)、尾递归优化(减少栈空间占用);

  • 二分应用:求 “木材切割的最大长度”(最小值最大化)、“包裹的最小容量”(最大值最小化)。

posted @ 2026-03-27 14:43  CodeMagicianT  阅读(5)  评论(0)    收藏  举报