二分查找
问题
问题描述
有一个有序序列
样例输入
- 第一行两个整数
,表示序列元素的数量,和有多少询问 - 第二行,
个数表示序列的元素,保证有序 - 接下来
行,询问的值
5 3
1 2 5 9 100
2
3
20
样例输出
- 每行两个数,表示每个询问的第一个
的值和对应的位置
2 2
5 3
100 5
解析1: 暴力
本问题是:在一个单调序列上查找
显示对于每个询问,最简单的方法就是使用暴力查询,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;
const int maxn=qe5+5;
int a[maxn];
int n,m;
int find(int val) {
for (int i = 1; i <= n; i++)
{
if (a[i] >= val)
return i;
}
return n+1;
}
int main() {
cin >> n >> m;
for(int i =1;i<=n;i++ )
cin >> a[i];
for(int i =1;i<=m;i++) {
int num;
cin >> num;
int pos = find(num);
if( pos == n+1) //没有找到
cout << "not found\n";
else
cout << a[pos] << " " << pos << "\n";
}
return 0;
}
上面的代码的时间复杂度为
解析2: 二分法
面对样例
我们可以利用元素有序这条性质来加快我们的查询呢?
一个朴素的想法如下
面对样例,我们查询的位置的范围是
同样,我们可以这样思考,我们把原序列末尾添加一个无穷大的值,改成
那么如果出现比最大的一个元素还大的查询,返回的位置就是not found
如果需要查询的元素是
- 我们成功的将问题缩小了一半,将问题变得到更简单了
- 新的问题与原问题是相似的,这显然是递归
- 小朋友法:如果找到足够多的小朋友来帮我们解题,一定可以解出来.所以问题有解.
下面采用更抽象的方法,来思考问题,设问题为
则得到公式如下
其中
于是我们写出如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
using namespace std;
const int maxn=1e5+5;
int a[maxn];
int n,m;
int mid(int l,int r) {
return (l+r) >> 1; //这是最快的写法
}
//检查pos位置的值是否符合要求
bool check(int pos,int val){
return a[pos] >= val;
}
//bs_find = binary search find
int bs_find(int l,int r,int val) {
while( l < r) {
int m = mid(l,r);
if( check(m,val)) //成立
r = m;
else //不成立,抛弃左半边
l = m+1;
}
return l ;
}
int main() {
cin >> n >> m;
for(int i =1;i<=n;i++ )
cin >> a[i];
for(int i =1;i<=m;i++) {
int num;
cin >> num;
int pos = bs_find(1,n+1,num);
if( pos == n+1) //没有找到
cout << "not found\n";
else
cout << a[pos] << " " << pos << "\n";
}
return 0;
}
总结
定义1 二分性
在一个序列
得到一个类似的如下的图:
如果序列b满足两个条件
- 只有两个值
- 相同值的位置都是连续
则说明序列
引理
一个具有二分性的序列不会出现蓝红蓝或红蓝红的连续三个相邻值
证明:
必要性显然.充分性: 根据描述,序列最多出现蓝红或红蓝,这种情况可以是边界,其它地方要么蓝,要么红,符合二分性的定义.
引理
一个序列
- 若某个位置
是 蓝,则所有小于位置的都是 蓝 - 若某个位置
是 红,则所有大于位置的都是 红
证明:
必要性显然. 充分性: 使用反证法: 假设序列蓝红蓝的情况,但此时违反了引理
推论1
具有二分性的序列可以使用二分查找算法来查找分界点的位置
推论2
对于序列
推论2证明:
二分查找的正确性证明
开始证明:
使用归纳法,证明bs_find(l,r,val)代码的正确性
只有1个元素的时候
分类讨论如下
- 情况1:
, check函数成立,则问题变成,得到正确答案 - 情况2:
, check函数不成立,则问题变成,得到正确答案
结论:当只有一个元素bs_find可以得到正确答案
有两个元素mid函数的运算后一定得到
分类讨论如下
- 情况1:
, check函数成立,则问题变成,得到正确答案 - 情况2:
, check函数不成立,则问题变成,得到正确答案
结论:当只有两个元素bs_find可以得到正确答案
同理,有bs_find(i,i+2,val)可以得到正确的答案
有
分类讨论如下
- 情况1:
, check函数成立,则问题变成, - 情况2:
, check函数不成立,则问题变成
于是问题就变成是一个长度为
同理,当有
这样一直到分解,最后剩余的元素个数,一定会变成
证明完毕.
一般化
TODO 没有解释清楚
具体什么样的情况下可以使用二分查询算法呢?
在一个序列
序列
得到一个类似的如下的图:
若序列 存在某个位置
什么是单调性?
单调性的性质:
在一个单调性的序列上
- 查询使函数
第一个成立的位置 - 查询使函数
最后一个失败的后一个的位置
可以使用二分查询法
什么题目使用
- 第一个
元素的位置 - 第一个
元素的位置 - 最后一个
元素的位置 - 求解数字
的数量 - 求解满足数字
的数量
经典模型
- 最小值最大,最大值最小问题
- 1.序列划分
二分的应用
-
在有序的数组里查找东西
- 查找一个特定的数字。
- 查找第一个大于或等于某个数的元素。
- 查找第一个大于某个数的元素。
- 查找最后一个小于或等于某个数的元素。
- 统计某个数出现了多少次。
-
猜答案(答案二分)
- 解决“最大值最小化”问题。
- 例子:把一根很长的木头切成
k段,怎么切才能让最长的那一段尽可能短?
- 例子:把一根很长的木头切成
- 解决“最小值最大化”问题。
- 例子:在一条路上装
k个路灯,怎么装才能让两盏灯之间的最小距离尽可能远?
- 例子:在一条路上装
- 解决“最大值最小化”问题。
-
在小数(实数)上查找
- 计算一个数的平方根,精确到指定的小数位。
- 求解方程的近似解。
-
其他常见应用
-
寻找“山峰”数组的顶点(一个数组先递增后递减,找最大值)。
这种问题的关键在于,虽然整个数组不是单调的,但我们可以利用山峰的性质来确定二分的搜索方向。山峰的左边是“上坡路”,右边是“下坡路”。
核心思想: 我们每次取中间位置
mid,通过比较A[mid]和它右边紧邻的元素A[mid+1]的关系,就能判断出mid正处于上坡还是下坡,从而确定真正的山峰在它的左边还是右边。具体步骤:
- 设置查找区间
[left, right]为整个数组的下标范围。 - 取中间位置
mid。 - 比较
A[mid]和A[mid+1]:- 如果
A[mid] < A[mid+1]:这说明mid正处在上坡阶段,那么真正的山峰一定在mid的右边(可能是mid+1或更远)。因此,我们可以安全地舍弃包括mid在内的左半部分,更新left = mid + 1。 - 如果
A[mid] > A[mid+1]:这说明mid正处在下坡阶段,那么山峰就是mid本身,或者在mid的左边。我们不能排除mid本身,因此可以舍弃mid右边的部分,更新right = mid。
- 如果
- 当
left和right相遇时,那个位置就是山峰的顶点。
示例代码 (C++):
cppcopy1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30#include <iostream> #include <vector> // 寻找山峰元素的下标 int findPeakElement(const std::vector<int>& nums) { int left = 0; int right = nums.size() - 1; while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] < nums[mid + 1]) { // mid 在上坡,山峰一定在右侧 left = mid + 1; } else { // mid 在下坡或就是山峰,答案在左侧或就是 mid right = mid; } } // 循环结束时,left == right,指向山峰 return left; } int main() { std::vector<int> mountain = {1, 3, 5, 8, 10, 7, 4, 2}; int peak_index = findPeakElement(mountain); std::cout << "山峰的下标是: " << peak_index << std::endl; std::cout << "山峰的值是: " << mountain[peak_index] << std::endl; return 0; } - 设置查找区间
-
加速某些算法,比如用 O(n log n) 的时间复杂度求解“最长上升子序列”。
-
练习题目
- luogu P1083
- luogu P2678
- luogu P1314
- luogu P1868
- luogu P1493
- hdu 6231
- poj 3273
- poj 3258
- poj 1905
- poj 3122
- luogu P1419 二分+单调队列