浅谈二分查找

如果我们有一个性质,能将一个区间划分成两部分,二分能将这个性质的边界求出来。
能二分的区间不一定具有单调性,但是具有单调性的区间一定能二分。

选自力扣

定义

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

原理

以在一个升序数组中查找一个数为例。 它每次考察数组当前部分的中间元素,如果中间元素刚好是要找的,就结束搜索过程;如果中间元素小于所查找的值,那么左侧的只会更小,不会有所查找的元素,只需到右侧查找;如果中间元素大于所查找的值同理,只需到左侧查找。

性质

二分查找的最优时间复杂度为 O(1) 。 二分查找的平均时间复杂度和最坏时间复杂度均为 O( log N )。因为在二分搜索过程中,算法每次都把查询的区间减半,所以对于一个长度为 的数组,至多会进行 O( log N ) 次查找。

以下是典例:

整数二分

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

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

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

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

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

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

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

如果数组中不存在该元素,则返回 -1 -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

#include<iostream>
using namespace std;
const int N = 100010;
int a[N];
int main()
{
    int n,m;
    cin >> n >> m;
    for( int i = 0; i < n; i ++ ) cin >> a[i];
    while( m -- )
    {
        int x;
        cin >> x;
        
        int l = 0, r = n - 1;
        while( l < r )
        {
            int mid = ( l + r ) / 2;
            if( a[mid] >= x ) r = mid;
            else l = mid + 1;
        }
        
        if( a[l] != x ) cout << "-1 -1" << endl;
        else 
        {
            cout << l << ' ';
            int l = 0, r = n - 1;
            while( l < r )
            {
                int mid = ( l + r + 1 ) / 2;
                if( a[mid] <= x ) l = mid;
                else r = mid - 1;
            }
            cout << l << endl;
        }
    } 
    return 0;
}

实数二分

数的三次方根 传送门

给定一个浮点数 n,求它的三次方根。

输入格式
共一行,包含一个浮点数 n。

输出格式
共一行,包含一个浮点数,表示问题的解。

注意,结果保留 6 位小数。

数据范围
−10000≤n≤10000

输入样例:
1000.00

输出样例:
10.000000

#include <iostream>
using namespace std;
int main()
{
    double n;
    cin >> n;
    double l = -1000, r = 1000;
    while( r - l > 1e-8 )
    {
        double mid = ( l + r ) / 2;
        if( mid * mid * mid >= n ) r = mid;
        else l = mid;
    }
    printf("%.6lf",l);
    return 0;
}

有人会问为什么要 while( r - l > 1e-8 ),而不是 r - l > 1e-6 题目不是结果保留 6 位小数吗?
这是口耳相传的书上没有,精度问题我们一般保留比题目要多加 2,这样精度更准。

注意,整数二分与实数二分有区别,区别在于实数更新边界时是 l=midr=mid,不能加 1,因为实数是连续的,加 1 会漏掉答案。

最大化与最小化

分巧克力 传送门

儿童节那天有 K 位小朋友到小明家做客。

小明拿出了珍藏的巧克力招待小朋友们。

小明一共有 N 块巧克力,其中第 i 块是 Hi×Wi 的方格组成的长方形。

为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。

切出的巧克力需要满足:

形状是正方形,边长是整数
大小相同
例如一块 6×5 的巧克力可以切出 6 块 2×2 的巧克力或者 2 块 3×3 的巧克力。

当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?

输入格式
第一行包含两个整数 N 和 K。

以下 N 行每行包含两个整数 Hi 和 Wi。

输入保证每位小朋友至少能获得一块 1×1 的巧克力。

输出格式
输出切出的正方形巧克力最大可能的边长。

数据范围
1≤N,K≤105,
1≤Hi,Wi≤105

输入样例:
2 10
6 5
5 6

输出样例:
2

思路:
假设答案是 mid ,如果每块巧克力的长和宽分别除 mid,再相乘,就是一块巧克力能分的最大数量,把每块巧克力分的量累加,满足题目要求的数量,答案就出来了。

#include <iostream>
using namespace std;
int const N = 100010;
int w[N], h[N];
int n, k;
bool check(int a)
{
    int num = 0;
    for (int i = 0; i < n; i ++)
    {
        num += (w[i] / a) * (h[i] / a);
        if (num >= k) return true;
    }
    return false;
}
int main()
{
    cin >> n >> k;
    for (int i = 0; i < n; i++) cin >> h[i] >> w[i];
    int l = 1, r = 1e5;
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    cout << r;
}

剪绳子 传送门
有 N 根绳子,第 i 根绳子长度为 Li,现在需要 M 根等长的绳子,你可以对 N 根绳子进行任意裁剪(不能拼接),请你帮忙计算出这 M 根绳子最长的长度是多少。

输入格式
第一行包含 2 个正整数 N、M,表示原始绳子的数量和需求绳子的数量。

第二行包含 N 个整数,其中第 i 个整数 Li 表示第 i 根绳子的长度。

输出格式
输出一个数字,表示裁剪后最长的长度,保留两位小数。

数据范围
1≤N,M≤100000,
0<Li<109

输入样例:
3 4
3 5 4

输出样例:
2.50

样例解释
第一根和第三根分别裁剪出一根 2.50 长度的绳子,第二根剪成 2 根 2.50 长度的绳子,刚好 4 根。

思路同上,都是假设答案为 mid ,二分找最大值。

#include<iostream>
using namespace std;
const int N=100010;
int a[N];
int n,m;

bool check(double mid)
{
    int cnt = 0;
    for(int i = 0; i < n; i ++)
    cnt += a[i] / mid;
    return cnt >= m;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 0; i < n; i ++) scanf("%d",&a[i]);

    double l = 0, r = 1e9;
    while(r - l > 1e-3)
    {
        double mid = (l + r) / 2;
        if(check(mid)) l = mid;
        else r = mid;
    }
    printf("%.2lf",l);
    
    return 0;
}

最小化同样可以用上面的思路

在线性结构上二分

可以用二分的要求:线性表能够根据中间元素的特点推测它两侧元素的性质,以达到缩减问题规模的效果即可,不一定非要有序。

寻找峰值 传送门

峰值元素是指其值大于左右相邻值的元素。

给你一个输入数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞ 。
示例 1:

输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。
示例 2:

输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。

提示:
1 <= nums.length <= 1000
-231 <= nums[i] <= 231 - 1
对于所有有效的 i 都有 nums[i] != nums[i + 1]

选自力扣
思路:
如果nums[mid] > nums[mid + 1],说明是递减的,峰值就不会在右区间,只会在左区间,所以r = mid

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

如果线性结构存的元素是局部单调,可以分成两部分,同样可以二分。

搜索旋转排序数组 传送门

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

示例 1:

输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:

输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:

输入:nums = [1], target = 0
输出:-1

提示:
1 <= nums.length <= 5000
-10^4 <= nums[i] <= 10^4

nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-10^4 <= target <= 10^4

思路:
对于有序数组,可以使用二分查找的方法查找元素。
我们可以在常规二分查找的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid][mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:

如果 [l, mid - 1] 是有序数组,且 target 的大小满足([nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
如果 [mid, r] 是有序数组,且 target 的大小满足 (nums[mid+1],nums[r]),则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。

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

STL 的二分查找

C++ 标准库中实现了查找首个不小于给定值的元素的函数 lower_bound ,lower_bound()返回值是一个迭代器,返回第一个大于等于查找值的指针。
假设数组 arry[ ] 里有n 个数,x 是你想要的答案

int mid = lower_bound(arry, arry + n, x) - arry;

查找首个大于给定值的元素的函数 upper_bound,upper_bound()返回第一个大于查找值的指针。
二者均定义于头文件 <algorithm> 中。
二者均采用二分实现,所以调用前必须保证元素有序。

二分的运用还有很多,笔者目前入坑尚浅,后续会继续补充~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值