算法学习入门---二分查找(C++)

目录

1.STL中的二分查找

2.牛客网---牛可乐和魔法封印

3.洛谷---A-B数对

4.洛谷---烦恼的高考志愿

5.洛谷---木材加工

6.洛谷---砍树

7.洛谷---跳石头


1.STL中的二分查找

lower_bound:大于等于 x 的最小元素,返回的是迭代器指针;时间复杂度为 O(logN)

使用时有3个参数,lower_bound(begin,end,target)

begin与end是查找的区间,左闭右开即 [begin,end),所以查找 a 数组 [1,8] 区间的target值,表示成 lower_bound(a+1,a+9,target)

upper_bound:大于 x 的最小元素,返回的是迭代器指针;时间复杂度为 O(logN)

返回的是大于x的最小元素迭代器,其他与lower_bound同


在使用迭代器时,可以使用auto类型来接受lower_bound返回的结果

然后对该结果进行解引用,即 auto it =lower_bound(begin,end,target),*it 即为it下标的值(相当于返回了一个只有一个值的数组)

注:STL中的二分算法只能对有序数组进行使用,使用的头文件为 algorithm

2.牛客网---牛可乐和魔法封印

非严格单调递增:递增的情况下,可以突然平一下,比如[1,2,2,3]

严格单调递增:递增的情况下,不能突然平一下

用二分查找左端点与二分查找右端点来解决问题(解决思路在leetcode二分查找中讲解了)

但需要注意的是几个特殊情况,因此该题不推荐使用lower_bound、upper_bound来完成,会使题目变得更加复杂(特殊情况在代码注释中有详细说明)

代码:

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;

int l_search(vector<long>& arr,long target)
{
    //需要特判,示例一中[-1,4]的情况
    int n = arr.size();
	if(target<arr[0]) return 0;
	int left=0,right=n-1;
	while(left<right)
	{
		int mid = left+(right-left)/2;
		if(target>arr[mid]) left = mid+1;
		else right = mid;
	}
	return left;
}

int r_search(vector<long>& arr,long target)
{
	int n = arr.size();
	//极限情况需要特判,例如示例一中的[2,6]
	if(target>arr[n-1]) return n-1; 
	int left=0,right=n-1;
	while(left<right)
	{
		int mid = left+(right-left+1)/2;
		if(target>=arr[mid]) left = mid;
		else right = mid-1;
	}
	return left;
}


int main()
{
    
	int n;
	cin>>n;
	vector<long> arr(n,0);
	for(int i=0;i<=n-1;i++) cin>>arr[i];
	int q;
    cin>>q;
	for(int i=1;i<=q;i++) 
	{
		long ret_left,ret_right;
		cin>>ret_left>>ret_right;
		if(ret_right<arr[0]||ret_left>arr[n-1]) cout<<0<<endl;//以示例一为例,[-1,0] [6,7] 结果都为0 
        else 
            cout<<(r_search(arr,ret_right)-l_search(arr,ret_left)+1)<<endl;//返回的是数组下标,因此结果处需要+1 
	}
	return 0;
} 

3.洛谷---A-B数对

需要的结果是 A-B = C,可以转换为 B = C - A ,这样我们需要求的值就从2个变为了1个,然后找到第一个为B的,再找到最后一个为B的,统计这个区间有多少个B,然后对每个A进行该操作即可。(如下图所示)

此处可以用求左右端点的模板来解决该题,也可以使用upper_bound与lower_bound,因为该题的极限条件没有上一道题目那么多,所以建议使用后者

upper_bound:返回B所在的下标指针

lower_bound:返回比B刚好大1的下标指针,由于是针对 [0,i-1] 区间进行该操作(把 i 位置的元素视作A),所以即使返回了 i 位置的指针也依然满足条件

两个指针,可以通过指针相减的方式,直接获得结果值;只要注意循环从 1 位置 开始循环即可,因为数对数对,一定得是一对数才行,i 位置处的数即为数对中的一个数

同时还需要对原数组排序,排序完再使用两个二分stl库函数

代码:

cpp 复制代码
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
#define int long long

signed main()
{
	int N,C;
	cin>>N>>C;
	vector<int> arr(N,0);
    for(int i=0;i<=N-1;i++)cin>>arr[i];
    sort(arr.begin(),arr.end());
	int ret = 0;
	for(int i=1;i<=N-1;i++)
	{
		int A = arr[i]; 
		auto x = lower_bound(arr.begin(),arr.begin()+i,A-C);
		auto y = upper_bound(arr.begin(),arr.begin()+i,A-C);
		ret+=(y-x);
	}
	cout<<ret;
	return 0;
}

代码易错点:

upper_bound 和 lower_bound 中,如果要表示数组首元素,就必须以 arr.begin() 来表示,人为规定如此

4.洛谷---烦恼的高考志愿

排序+二分,可以使用lower_bound来辅助解决,需要注意的是score比最小的分数还要小以及比最大的分数还要大两种情况,特判完即可

代码:

cpp 复制代码
#include<algorithm>
#include<vector>
#include<iostream>
#include<cmath>
using namespace std;
#define int long long

signed main()
{
	int m,n;
	cin>>m>>n;
	vector<int> schools(m,0);
	for(int i=0;i<=m-1;i++) cin>>schools[i];
	sort(schools.begin(),schools.end());
	int ret = 0;
	for(int i=1;i<=n;i++)
	{
		int score;
		cin>>score;
		if(score<schools[0]) ret += (schools[0]-score);
		else if(score>schools[m-1]) ret += (score-schools[m-1]);
		else
		{
			auto it = lower_bound(schools.begin(),schools.end(),score);
			int school1 = *it;
			int school2 = *(it-1);
			if(fabs(score-school1)>fabs(score-school2)) ret+=fabs(score-school2);
			else ret+=fabs(score-school1);
		}
	}
	cout<<ret;
	return 0;
}

算法题小诀窍:当发现类型错误时,#define int long long + int main 改为 signed main 可以解决大多数问题

5.洛谷---木材加工

如下图所示,以两根原木分别 11cm 和 21 cm 为例,每段切为5cm的话,即11cm的切出2段,21cm的切出4段,总共6段;如果每段切为4cm的话,可以切出7段,依旧满足条件,但不是最优解

解法1:枚举从 0 cm到 maxlen cm(最长的一根原木的长度)的所有情况,每次判断一遍能切出多少段木头;对于第 i 根木头,可以切出 a[i] / x 段木头,所以假如 c 代表总的切出来的木段数,那么 c += a[i] / x(x为当前切的每段长度)


解法2:二分答案

不难发现 x 与 c 成反比,当 x 为 maxlen 的时候只能切一段,为 1 时能且非常多段,这就是题目的二段性;如下图所示,c(切出来的段数)大于等于k时,说明 x 比较小落在了左边区间内;c 小于k时,说明 x 比较大落在了右边区间;当处于某个 x 值时,c 恰好处于 >=k 与 <k 的界限上,那么就是最后的ret,所以可以把题目转换为寻找区间右端点来解决

寻找区间右端点:left = mid and right = mid - 1,c >= k and c < k

c 的段数可以通过一个 caculator 函数来求出,通过模块性来降低代码的整体复杂度


总结:二分答案为从一堆答案当中,通过二分查找的方式找到最优解,它可以处理大部分[最小的最大值] 和 [最大的最小值] 问题,只要有二段性即可解决问题

代码:

cpp 复制代码
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int caculator(vector<int>& a,int x)//统计段数 
{
	int ret = 0;
	for(int i=0;i<=a.size()-1;i++)
		ret+=a[i]/x;
	return ret;
}

int main()
{
	int n,k;
	cin>>n>>k;
	vector<int> forests(n,0);
	for(int i=0;i<=n-1;i++)cin>>forests[i];
	sort(forests.begin(),forests.end());
	int left=0,right=forests[n-1];
	while(left<right)
	{
		int mid = left+(right-left+1)/2;
		if(caculator(forests,mid)>=k) left = mid;
		else right = mid - 1;
	}
	cout<<left;
	return 0;
}

6.洛谷---砍树

如下图所示,最优解是15cm高的电锯切一刀,20cm的切出5cm,17cm的切出2cm,最后刚好是要求的7cm,现在求这个电锯最高可以放在哪里切

二分答案:

与上题解题思路大致相同,假设 h 表示当前电锯高度,c 表示当伐木机高度为 x 时能切出来的厘米数,则 h 与 c 成反比;所以 c >= M 时,h 在左区间,c < M 时,h 在右区间;对 [0,maxlen] 区间的值进行二分查找,maxlen 为最长的树木长度

根据上题的模板,只要把caculator函数稍微修改一下,就能够ac掉这道题了

注:本题数据大小会比较大,因此需要 #define int long long + signed main

代码:

cpp 复制代码
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
#define int long long
int caculator(vector<int>& a,int x)//统计厘米 
{
	int ret = 0;
	for(int i=0;i<=a.size()-1;i++)
		if(a[i]>x) ret+=(a[i]-x);
	return ret;
}

signed main()
{
	int n,k;
	cin>>n>>k;
	vector<int> forests(n,0);
	for(int i=0;i<=n-1;i++)cin>>forests[i];
	sort(forests.begin(),forests.end());
	int left=0,right=forests[n-1];
	while(left<right)
	{
		int mid = left+(right-left+1)/2;
		if(caculator(forests,mid)>=k) left = mid;
		else right = mid - 1;
	}
	cout<<left;
	return 0;
}

7.洛谷---跳石头

L距离之间有N+2块石头,现在可以从中移除2块(不能是第一块/最后一块),要求移除后 距离最近的两块相邻石头 距离最远,N行数分别代表距离第1块石头的距离

二分答案:

当我们看到最短跳跃距离要尽可能长的这种字眼,就要往 动态规划/二分答案/贪心 上面去思考

把 x 设为最短跳跃距离,c 设为跳跃距离为 x 下移走的岩石数目;假设把所有石头都移走,那么就得从起点一步跳到终点,通过该极限情况很容易发现 c 与 x 成正比

难点解析:

本题的难点在于,如何求出在跳跃距离为 x 的情况下,移走的岩石数目

可以通过一个双指针的方式来解决,定义双指针 i 、j ,初始都指向起点位置;然后 j 开始向后移动,直到移动到 a[j] - a[i] >= x 的情况下,此时 j - i - 1 即为所需移除的石头数量;然后 i 不要一步步往后移动到 j 位置,直接移到 j 位置即可,因为 i 移动时 j 已经处于最优的情况了,i 不断移动以后只会间隔越来越小,距离 x 也越来越远;最后对整个岩石数组重复该操作,并把每次情况统计到 ret 变量,因为 x 是最短跳跃距离,要所有的相邻石头都满足这个 x

代码:

cpp 复制代码
#include<iostream>
#include<vector>
#include<limits.h>
using namespace std;
#define int long long
int caculator(vector<int>& a,int x)
{
	int ret = 0;
	int i = 0,j = 0,n = a.size();
	while(i<=n-1)
	{
		j = i;
		while(j<=n-1&&a[j]-a[i]<x)j++;
		ret += (j-i-1);
		i = j;
	}
	return ret;
}

signed main()
{
	int L,N,M;
	cin>>L>>N>>M;
	vector<int> stones(N+2,0);
	stones[N+1] = L;
	for(int i=1;i<=N;i++) cin>>stones[i];
	//开始二分答案
	int left = 0,right = L;
	while(left<right)
	{
		int mid = left+(right-left+1)/2;
		if(caculator(stones,mid)<=M) left = mid;
		else right = mid - 1;
	}
	cout<<left; 
	return 0;
}

代码易错点:

当 i = j = 3时,j 遍历完以后都没有把4、5两块石头给统计进去,同时最小的距离也不为 x ,14 与 25 之间只距离了 11,所以肯定出现逻辑错误;但我们可以假设把最后一块石头也给移出,这样14 与 +∞ 距离就能够大于 x 了,同时移除数量也是不会影响结果的

相关推荐
雪域迷影1 小时前
C++中编写UT单元测试用例时如何mock非虚函数?
开发语言·c++·测试用例·gmock·cpp-stub开源项目
sheeta19983 小时前
LeetCode 每日一题笔记 日期:2025.11.24 题目:1018. 可被5整除的二进制前缀
笔记·算法·leetcode
是小胡嘛5 小时前
C++之Any类的模拟实现
linux·开发语言·c++
Want5958 小时前
C/C++跳动的爱心①
c语言·开发语言·c++
lingggggaaaa8 小时前
免杀对抗——C2远控篇&C&C++&DLL注入&过内存核晶&镂空新增&白加黑链&签名程序劫持
c语言·c++·学习·安全·网络安全·免杀对抗
phdsky8 小时前
【设计模式】建造者模式
c++·设计模式·建造者模式
H_-H8 小时前
关于const应用与const中的c++陷阱
c++
coderxiaohan8 小时前
【C++】多态
开发语言·c++
gfdhy9 小时前
【c++】哈希算法深度解析:实现、核心作用与工业级应用
c语言·开发语言·c++·算法·密码学·哈希算法·哈希
百***06019 小时前
SpringMVC 请求参数接收
前端·javascript·算法