二分

一、二分查找(Binary Search) 典型场景:在 有序数组 中查找目标值,或查找满足某种单调性质的边界。

  1. 标准二分查找(精确匹配)
// 在有序数组 nums 中查找 target,返回下标,若不存在返回 -1
int binarySearch(vector<int>& nums, int target) {
    int left = 0, right = nums.size() - 1;
    while (left <= right) {               // 闭区间 [left, right]
        int mid = left + (right - left) / 2;  // 防溢出
        if (nums[mid] == target)
            return mid;
        else if (nums[mid] < target)
            left = mid + 1;
        else
            right = mid - 1;
    }
    return -1;
}
2. 查找第一个 ≥ target 的位置(lower_bound)
cpp
// 返回第一个大于等于 target 的下标,若全小于则返回 nums.size()
int lowerBound(vector<int>& nums, int target) {
    int left = 0, right = nums.size();      // 左闭右开 [left, right)
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] >= target)
            right = mid;                    // 满足条件,向左收缩
        else
            left = mid + 1;
    }
    return left;  // 或 right,两者相等
}
3. 查找第一个 > target 的位置(upper_bound)
cpp
// 返回第一个大于 target 的下标
int upperBound(vector<int>& nums, int target) {
    int left = 0, right = nums.size();
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > target)
            right = mid;
        else
            left = mid + 1;
    }
    return left;
}

模板要点 区间定义:

精确查找常用闭区间 [left, right],循环条件 left <= right,更新为 mid ± 1。

边界查找(如 lower_bound)常用左闭右开 [left, right),循环条件 left < right,更新为 right = mid 或 left = mid + 1。

防溢出:mid = left + (right - left) / 2。

C++ STL 已有函数:lower_bound、upper_bound、binary_search,但手写模板更灵活应对复杂判断条件。

二、二分答案 —— 求最大值最小 核心思想:当问题具有 单调性(即若某个答案 x 可行,则所有 ≥ x 的答案都可行,或反之),我们可以 二分答案,然后用一个判定函数 check(mid) 判断 mid 是否可行,从而收缩区间。

求最大值最小 的一般特征:在满足条件的前提下,使 最大值尽可能小。

通用模板(最大值最小)

// 假设答案范围在 [L, R] 内
int left = L, right = R;
int ans = -1;
while (left <= right) {
    int mid = left + (right - left) / 2;
    if (check(mid)) {       // 若 mid 可行,则尝试更小的值
        ans = mid;
        right = mid - 1;
    } else {
        left = mid + 1;     // 不可行,必须增大 mid
    }
}
return ans;

示例:分割数组的最大值(LeetCode 410) 给定非负整数数组和整数 m,将数组分割成 m 个连续子数组,使得子数组和的最大值最小。

bool check(vector<int>& nums, int m, int maxSum) {
    int cnt = 1, curSum = 0;
    for (int num : nums) {
        if (curSum + num > maxSum) {
            cnt++;
            curSum = num;
            if (cnt > m) return false;
        } else {
            curSum += num;
        }
    }
    return true;
}

int splitArray(vector<int>& nums, int m) {
    int left = *max_element(nums.begin(), nums.end());  // 最小可能值
    int right = accumulate(nums.begin(), nums.end(), 0); // 最大可能值
    int ans = right;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (check(nums, m, mid)) {
            ans = mid;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return ans;
}

模板要点 单调方向:check(mid) 返回 true 表示当前最大值 mid 可行,此时我们可以尝试 更小的最大值,因此 right = mid - 1。

答案记录:因为 mid 可行时可能是最终答案,故用 ans 记录,最后返回。

三、二分答案 —— 求最小值最大 典型场景:在满足条件的前提下,使 最小值尽可能大。与上一类方向相反。

通用模板(最小值最大)

int left = L, right = R;
int ans = -1;
while (left <= right) {
    int mid = left + (right - left) / 2;
    if (check(mid)) {       // 若 mid 可行,则尝试更大的值
        ans = mid;
        left = mid + 1;
    } else {
        right = mid - 1;    // 不可行,必须减小 mid
    }
}
return ans;

示例:在 D 天内送达包裹的能力(LeetCode 1011) 传送带上的包裹必须按顺序在 D 天内运完,求满足条件的最小运载能力(实际上这题也是求最大值最小,但思路相似)。这里举一个经典的最小值最大问题:Aggressive Cows(POJ 2456)

在一条直线上有 N 个牛棚,给定坐标,要放入 C 头牛,使得最近的两头牛之间的距离尽可能大。

bool check(vector<int>& stalls, int c, int dist) {
    int cnt = 1, lastPos = stalls[0];
    for (int i = 1; i < stalls.size(); ++i) {
        if (stalls[i] - lastPos >= dist) {
            cnt++;
            lastPos = stalls[i];
            if (cnt >= c) return true;
        }
    }
    return false;
}

int maxMinDistance(vector<int>& stalls, int c) {
    sort(stalls.begin(), stalls.end());
    int left = 1;                               // 最小可能距离
    int right = stalls.back() - stalls[0];      // 最大可能距离
    int ans = 0;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (check(stalls, c, mid)) {
            ans = mid;
            left = mid + 1;    // 可行,尝试更大的距离
        } else {
            right = mid - 1;
        }
    }
    return ans;
}

模板要点 单调方向:check(mid) 返回 true 表示当前最小距离 mid 可行,我们可以尝试 更大的最小值,因此 left = mid + 1。

与最大值最小的区别:仅在于可行时收缩区间的方向不同,本质上都是利用单调性。

四、实数区间二分 典型场景:求方程根、几何问题、概率或精度要求高的最优值。与整数二分不同,循环终止条件通常由 精度 eps 或 固定迭代次数 控制。

通用模板(精度控制)

const double eps = 1e-7;  // 精度要求

double left = L, right = R;
while (right - left > eps) {
    double mid = (left + right) / 2.0;
    if (check(mid))       // 根据单调性决定收缩方向
        right = mid;      // 或 left = mid
    else
        left = mid;
}
return left;  // 或 right,两者近似相等

示例:计算平方根(或解方程)

double sqrt(double x) {
    double left = 0, right = max(1.0, x);
    for (int i = 0; i < 100; ++i) {  // 固定迭代 100 次,足够安全
        double mid = (left + right) / 2.0;
        if (mid * mid <= x)
            left = mid;
        else
            right = mid;
    }
    return left;
}

模板要点 终止条件:

方式一:while (right - left > eps),注意 eps 应比要求精度再小 1~2 个数量级。

方式二:固定迭代次数(如 100 次),避免精度问题导致的死循环,适用于绝大多数场景。

区间更新:实数二分通常直接 left = mid 或 right = mid,无需 ±1 操作。

五、二分搜索综合练习题模板思路 综合题往往需要将问题 转化成二分判定 的形式,关键步骤为:

识别单调性:是否存在某个临界值,使得一侧全部满足条件,另一侧全部不满足。

设计判定函数 check(mid):这是二分答案的核心,需要高效(通常 O(n) 或 O(n log n))判断 mid 是否可行。

确定搜索区间:left 和 right 的初始值,通常为题目给定的明确边界或根据数据范围推算。

选择二分模板:根据是求“最大值最小”还是“最小值最大”确定收缩方向。

常见综合题型分类 题型 典型问题 判定函数设计要点 二分查找变形 搜索旋转排序数组、寻找峰值、有序矩阵第 K 小 利用局部有序性或二分缩小范围 二分答案 + 贪心判定 分割数组、最小化最大和、最大化最小距离、K 次操作后的最大/最小值 在给定 mid 下,用贪心策略模拟计算是否能在约束内完成 二分答案 + 前缀和/双指针 子数组和 ≥ K 的最短长度、第 K 小的子数组和 判定时可能需要滑动窗口、前缀和 + 二分查找 实数二分 高精度求根、三分求极值、几何覆盖问题 固定迭代次数,判定函数为数学计算 交互式二分 猜数字、第一个错误的版本 调用外部 API 作为 check 条件 综合示例:K 次操作后最大化的最小值 给定数组 nums,每次操作可以将某个元素加 1,最多操作 K 次,求操作后数组最小值最大是多少。

bool check(vector<int>& nums, long long k, int target) {
    long long need = 0;
    for (int num : nums) {
        if (num < target)
            need += target - num;
        if (need > k) return false;
    }
    return need <= k;
}

int maxMinAfterKOperations(vector<int>& nums, int k) {
    int left = *min_element(nums.begin(), nums.end());
    int right = left + k;  // 最大值不可能超过这个
    int ans = left;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (check(nums, k, mid)) {
            ans = mid;
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return ans;
}

六、二分常见易错点总结 易错点 说明与解决方法 死循环 整数二分更新时,确保每次区间至少缩小 1。常用 left = mid + 1 或 right = mid - 1。 mid 计算溢出 始终用 left + (right - left) / 2,而非 (left + right) / 2。 区间开闭混乱 选定一种区间定义(闭区间 [L,R] 或左闭右开 [L,R))并贯彻到底。 判定函数复杂度太高 二分答案的判定函数应尽可能 O(n) 以内,否则总时间可能超限。 答案未记录导致丢失 在可行时用 ans = mid 记录,避免最后 left/right 不一定是正确答案的情况。 实数二分精度问题 优先使用固定迭代次数(如 100 次),或设 eps 为要求精度的 1/100。