如果我们有一个性质,能将一个区间划分成两部分,二分能将这个性质的边界求出来。
能二分的区间不一定具有单调性,但是具有单调性的区间一定能二分。
定义
二分查找也称折半查找(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=mid
或 r=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>
中。
二者均采用二分实现,所以调用前必须保证元素有序。
二分的运用还有很多,笔者目前入坑尚浅,后续会继续补充~