算法——二分算法

算法——二分算法第一步 要判断题目是否具有二分性质

第一步,要判断题目是否具有二分性质。通常会根据二分性质设计一个check函数,有了该函数的返回值就能够很好的控制左右指针的移动。所以,难点在与提取二分性质(暂时没有学会很好的通用方法)。

一般用问题的类型来判断是否使用二分,然后从二分问题的类型来进一步推断二分性质是什么。

常见的二分题型:

  • 二分查找
  • 二分答案:求最小(最小化最大值)、求最大(最大化最小值)
  • K K K大、第 K K K

第二步,确定二分模板。注意左右区间的开闭,常用左闭右闭、左开右开。模板之间的转换在下面推导。

二分模板

题目:寻找已排序数组 n u m s nums nums中第一个大于等于指定元素 t a r g e t target target的位置。即, t a r g e t ≥ x target \ge x targetx

int lowerBound(vector<int>& nums, int target) {
   
    int l = 0, r = nums.size() - 1; // 闭区间[l, r]
    while (l <= r) {
    // 闭区间不为空
        int mid = l + (r - l) / 2; // 防止溢出 int 范围
        if (nums[mid] < target) {
   
            l = mid + 1; // [mid + 1, r]
        } else {
   
            r = mid - 1; // [l, mid - 1]
        }
    }
    // 上述式子确保:
    // nums[l - 1] < target
    // nums[r + 1] >= target
    // 也就是说 [r + 1, n - 1]区间所有元素大于等于 target
    // 则第一个大于等于 target 的元素位置即为 r + 1
    // 根据 while 循环的退出条件有 r + 1 == l
    return l;
}

其他情况可以由该位置推导:

  • t a r g e t > x ⇔ t a r g e t ≥ ( x + 1 ) target > x \Leftrightarrow target \ge (x + 1) target>xtarget(x+1)
  • t a r g e t < x ⇔ ( t a r g e t ≥ x ) − 1 target < x \Leftrightarrow (target \ge x) - 1 target<x(targetx)1
  • t a r g e t ≤ x ⇔ ( t a r g e t ≥ ( x + 1 ) ) − 1 target \le x \Leftrightarrow (target \ge (x + 1)) - 1 targetx(target(x+1))1
// 1. target >= x
int idx = lowerBound(nums, target);

// 2. target > x
int idx = lowerBound(nums, target + 1);

// 3. target < x
int idx = lowerBound(nums, target) - 1;

// 4. target <= x
int idx = lowerBound(nums, target + 1) - 1;

题目

在排序数组中查找元素的第一个和最后一个位置

class Solution {
   
    int lowerBound(vector<int>& nums, int target) {
   
        int l = 0, r = nums.size() - 1;
        while (l <= r) {
   
            int mid = l + (r - l) / 2;
            if (nums[mid] < target) {
   
                l = mid + 1;
            } else {
   
                r = mid - 1;
            }
        }
        return l;
    }
public:
    vector<int> searchRange(vector<int>& nums, int target) {
   
        int l = 0, r = nums.size() - 1;
        int left = lowerBound(nums, target);
        if (left == nums.size() || nums[left] != target) {
   
            return {
   -1, -1};
        }
        int right = lowerBound(nums, target + 1) - 1;
        return {
   left, right};
    }
};

寻找旋转排序数组中的最小值

  • 设每次二分取到的数为 x = n u m s [ m i d ] x=nums[mid] x=nums[mid]
  • 如果 x > n u m s [ n − 1 ] x>nums[n-1] x>nums[n1],那么最小值必定在 [ m i d + 1 , n − 1 ] [mid+1, n-1] [mid+1,n1];如果 x < n u m s [ n − 1 ] x<nums[n-1] x<nums[n1],那么最小值必定在 [ 0 , m i d − 1 ] [0, mid-1] [0,mid1]。具有二分性质。
  • 由于最小值是必定存在的,所以可以在 [ 0 , n − 2 ] [0, n-2] [0,n2]进行二分查找,如果 n u m s [ n − 1 ] nums[n-1] nums[n1]是最小值,那么每次 l l l会一直左移,直到 l = = n − 1 l == n - 1 l==n1跳出循环,最终答案就是 n u m s [ n − 1 ] nums[n-1] nums[n1]
class Solution {
   
public:
    int findMin(vector<int>& nums) {
   
        int l = 0, r = nums.size() - 1;
        int end = nums.back();
        while (l <= r) {
   
            int mid = l + (r - l) / 2;
            if (nums[mid] > end) {
   
                l = mid + 1;
            } else {
   
                r = mid - 1;
            }
        }
        return nums[l];
    }
};

搜索旋转排序数组

  • x = n u m s [ m i d ] x = nums[mid] x=nums[mid]为二分取到的数值, e n d = n u m s [ n − 1 ] end = nums[n-1] end=nums[n1]为数组的最后一个数值。用 x x x t a r g e t target target来寻找二分的性质。
  • 讨论 x x x t a r g e t target target右边,或等于 t a r g e t target target的情况。其余情况 x x x必然在 t a r g e t target target左边。
  • 如果 x > e n d x > end x>end,表示 x x x在第一段中,那么 t a r g e t target target必定要在第一段中(否则,如果 t a r g e t target target在第二段中,那么 x x x必然在 t a r g e t target target左边,与当前考虑情况矛盾),且 x x x必须大于等于 t a r g e t target target
  • 如果 x ≤ e n d x \le end xend,说明 x x x在第二段中,那么 t a r g e t target target可以在第一段,也可以在第二段。
    • 如果 t a r g e t target target在第一段,那么 x x x必然在 t a r g e t target target右边
    • 如果 t a r g e t target target在第二段,那么 x x x必须要大于等于 t a r g e t target target,才能使得 x x x位于 t a r g e t target target右边
  • 同样可以在 [ 0 , n − 2 ] [0, n-2] [0,n2]进行二分查找,如果 n u m s [ n − 1 ] = = t a r g e t nums[n - 1] == target nums[n1]==target,那么 l l l也会一直左移直到 l = = n − 1 l == n-1 l==n1跳出循环,所以只要最后判断 n u m s [ l ] nums[l] nums[l]是否和 t a r g e t target target相等即可。如果相等,则证明找到了;如果不相等,证明没找到。
class Solution {
   
    bool check(vector<int>& nums, int x, int target) {
   
        int end = nums.back();
        if (x > end) {
   
            return target > end && target <= x;
        } else {
   
            return target > end || target <= x;
        }
    }
public:
    int search(vector<int>& nums, int target) {
   
        int l = 0, r = nums.size() - 2;
        while (l <= r) {
   
            int mid = l + (r - l) / 2;
            if (check(nums, nums[mid], target)) {
   
                r = mid - 1;
            } else {
   
                l = mid + 1;
            }
        }
        return nums[l] == target ? l : -1;
    }
};

有序矩阵中第K小的元素

  • K K K小问题。
  • 二分性质:对于所有元素都位于 [ m i n V a l u e , m a x V a l u e ] [minValue,maxValue] [minValue,maxValue],对于某一个数 m i d mid mid可以统计出小于等于它的数的个数(具体方法参考官方题解),设个数为 c n t cnt cnt。那么, m i d mid mid越大, c n t cnt cnt越大; m i d mid mid越小, c n t cnt cnt越小。所以,二分查找 m i d mid mid的下界,使得 c n t ≥ k cnt \ge k cntk
class Solution {
   
    bool check(vector<vector<int>>& matrix, int k, int mid) {
   
        int n = matrix.size();
        int cnt = 0; // 统计小于等于 mid 的元素个数
        for (int i = 0, j = n - 1; i < n; i ++) {
   
            while (j >= 0 && matrix[i][j] > mid) j --;
            cnt += (j + 1);
        }
        return cnt < k;
    }
public:
    int kthSmallest(vector<vector<int>>& matrix, int k) {
   
        int n = matrix.size();
        int l = matrix[0][0], r = matrix[n - 1][n - 1];
        while (l <= r) {
   
            int mid = l + (r - l >> 1);
            if (check(matrix, k, mid)) {
   
                l = mid + 1;
            } else {
   
                r = mid - 1;
            }
        }
        return l;
    }
};

在D天内送达包裹的能力

  • 求最小问题。
  • 二分性质:运载能力越大,所用天数越少;运载能力越小,所用天数越多。所以,二分查找运载能力的下界,使得天数符合要求。
class Solution {
   
public:
    int shipWithinDays(vector<int>& weights, int days) {
   
        auto check = [&](int cap) {
   
            int cnt = 1, sum = 0;
            for (int w : weights) {
   
                if (w > cap) return false;
                if (sum + w <= cap) {
   
                    sum += w;
                } else {
   
                    cnt ++;
                    if (cnt > days) {
   
                        return false;
                    }
                    sum = w;
                }
            }
            return true;
        };

        int sum = 0;
        for (int w : weights) sum += w;
        int l = sum / days, r = sum;
        while (l <= r) {
   
            int mid = l + (r - l >> 1);
            if (check(mid)) {
   
                r = mid - 1;
            } else {
   
                l = mid + 1;
            }
        }
        return l;
    }
};
编程小号
上一篇 2025-03-19 20:40
下一篇 2025-03-03 12:33

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ji-chu/98216.html