算法:二分法

什么是二分?


我们首先来玩一个猜数游戏:我随便想一个1~100中的数字,

你的目标是以最少的次数猜到这个数字。

每次猜测后,我都会告诉你大了、小了或对了来验证结果。

有两种猜数方式:

第一种:假设你从1开始依次往上猜

这种方式的风险极高,假如我想的数是1,那么只用猜一次;假如我想的数是99,则需要猜测99次才能猜对。

第二种:假设你从50开始折半猜

1~100 猜50,小了。

50~100 猜75,大了。

50~75 猜62,大了。

50~62 猜56,对了!

这样每一次都取得当前可能区间的中间值,从而将余下的数排除一半。这种方法就叫做:二分法。


每一步的猜测都可以帮我们排除掉一半的错误结果,大大提高了查找答案的效率。

但是使用这个方法的首要前提是:需要查找的序列是有序排列的。

二分的实现


二分法最经典的写法应是如此:

在简单有序数组中查找指定元素

int binary_search(int low,int high,int key){
    while(low <= high){
        int mid = (low + high) / 2;
        if(arr[mid] > key){
            high = mid - 1;
        }
        else if(arr[mid] < key){
            low = mid + 1;
        }
        else return mid;//找到元素,返回对应下标
    }
    return -1;//没有找到元素
}

下面我们基于一些例子,对二分的写法进行一定的优化和拓展。

1.洛谷 P2249 【深基13.例1】查找

https://www.luogu.com.cn/problem/P2249

题目描述:

输入 n 个不超过 10^9 的单调不减的(就是后面的数字不小于前面的数字)非负整数 a1​,a2​,…,an​,然后进行 m 次询问。对于每次询问,给出一个整数 q,要求输出这个数字在序列中第一次出现的编号,如果没有找到的话输出 −1 。

首先看题目完全符合二分法的使用情景,但是与简单二分查找不一样的是,我们需要找到这个数字第一次出现时的位置。

AC代码如下: 

#include <iostream>
using namespace std;

int m,n;
int que;
int arr[1000005]; 

int BSearch(int x){
	int low = 1;
	int high = n;
	while(low < high){
		int mid = low + (high-low) / 2; 
		if(x <= arr[mid]) high = mid;
		else if(x > arr[mid]) low = mid + 1;
	}
	if(x == arr[low]){
		return low; 
	}
	else return -1;
}

int main(){
	scanf("%d %d",&n,&m);
	for(int i = 1;i <= n;i++){
		scanf("%d",&arr[i]);
	}
	for(int k = 1;k <= m;k++){
		scanf("%d",&que);
		int ans = BSearch(que);
		printf("%d ",ans);
	}
	return 0;
}

 单独来看二分语句块:

int BSearch(int x){
	int low = 1;
	int high = n;
	while(low < high){//循环终止条件变化
		int mid = low + (high-low) / 2; //这样写可以避免爆出int范围
		if(x <= arr[mid]) high = mid;//判断相等时并不直接输出,high的变化语句也有所改变
		else if(x > arr[mid]) low = mid + 1;
	}
	if(x == arr[low]){
		return low; 
	}
	else return -1;
}

这样的写法也完全可以满足经典写法下的查找需求。

这里有几个值得思考的点,自己举出一些例子模拟一遍可以加深理解:

1.当循环终止时,low和high满足什么条件?

2.以下不同情形,最终返回值分别处于什么位置?

        ①查找值不存在,小于序列中所有数

        ②查找值不存在,大于序列中所有数

        ③查找值存在且唯一

        ④查找值存在但不唯一

        ⑤查找值不存在,但应在序列中部

3.最后的返回值是low,是否可以满足所有情形?

4.当low和high相邻时,二分的过程是怎么样的?


问题参考答案如下,可供参考:

1.low == high

2.①返回值指向第一个位置

   ②返回值指向最后一个位置

   ③返回值指向目标值位置

   ④返回值指向第一个目标值位置

   ⑤返回值总是low

3.4.当low和high相邻时,总是查询low指向的数,最终不管结果如何,low == high。最终总是low移动到(或指向)返回值的位置,所以可以满足所有情形。


二分答案

解题的时候往往会考虑枚举答案然后检验枚举的值是否正确。当答案处于一定范围,且满足单调性,以枚举的方式很难一一列举时,把枚举换成二分,就变成了「二分答案」。

2.洛谷 P1873 [COCI 2011/2012 #5] EKO / 砍树 

https://www.luogu.com.cn/problem/P1873

题目描述过长,大家可以自行跳转阅读

题目解读:我们可以在0到最高树高中枚举答案,但是从0枚举到最高树高太耗时间。我们可以在[0,max)的区间上进行二分作为答案,然后检查各个答案的可行性(一般使用贪心法)。这就是二分答案。

AC代码如下:

#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;

long long n;
long long m;
long long arr[1000005];

bool check(long long height){
	long long sum = 0;
	 for(int i = 1;i <= n;i++){
	 	if(arr[i] - height > 0){
	 		sum += arr[i] - height;
		}
	 }
	return sum >= m;
}

int main(){
	cin>>n>>m;
	long long max = -1;
	for(int i = 1;i <= n;i++){
		scanf("%d",arr + i);
		if(max <= arr[i]) max = arr[i];
	}
	long long low = 0;
	long long high = max;
	long long mid;
	while(low + 1 < high){//二分范围[0,max) 
		mid = low + (high - low) / 2;
		if(check(mid)) low = mid;
		else high = mid; 
	}
	cout<<low ;
	return 0;
}

 单独来看二分语句块:

long long low = 0;
long long high = max;
long long mid;
while(low + 1 < high){//二分范围[0,max) 
	mid = low + (high - low) / 2;
	if(check(mid)) low = mid;
	else high = mid; 
}
cout<<low ;

同样提出几个值得思考的点: 

1.与二分查找的语句之间有什么不同?

2.二分答案的范围应如何确定(视题目而定),这个很重要。

3.二分答案的范围区间为何是左闭右开?

4.为何返回值还是low?

5.思考如下图情况时,最终结果应是怎样?


 给出结果图,其他问题没有标准答案,自己思考的结果就是问题的答案。

经过这一篇文章的学习,赶紧去做几个题练练手吧!

有疑问欢迎在评论区指出,相互探讨和学习! 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是C++实现顺序查找算法二分法查找算法的示例代码: ```c++ #include <iostream> #include <vector> #include <algorithm> using namespace std; // 顺序查找算法 int seqSearch(vector<int>& nums, int target) { for (int i = 0; i < nums.size(); i++) { if (nums[i] == target) { return i; } } return -1; // 查找失败 } // 二分法查找算法 int binarySearch(vector<int>& nums, int target) { int left = 0, right = nums.size() - 1; while (left <= right) { int mid = (left + right) / 2; if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return -1; // 查找失败 } int main() { vector<int> nums = {3, 5, 2, 8, 4, 7, 1, 6}; sort(nums.begin(), nums.end()); // 二分法查找算法需要有序表 int target = 4; int index1 = seqSearch(nums, target); // 调用顺序查找算法 int index2 = binarySearch(nums, target); // 调用二分法查找算法 if (index1 != -1) { cout << "顺序查找成功,目标元素下标为:" << index1 << endl; } else { cout << "顺序查找失败,未找到目标元素" << endl; } if (index2 != -1) { cout << "二分法查找成功,目标元素下标为:" << index2 << endl; } else { cout << "二分法查找失败,未找到目标元素" << endl; } return 0; } ``` 以上代码中,我们使用了STL中的vector容器来存储顺序表元素,并使用sort函数对其进行排序,以便二分法查找算法能够正确执行。在main函数中,我们分别调用了顺序查找算法二分法查找算法,查找目标元素的值为4。最后,根据返回的下标值,输出查找结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值