当前位置: 首页 > news >正文

算法基础—二分算法

目录

一、⼆分查找例题

1 牛可乐和魔法封印

2 A-B 数对

3 烦恼的高考志愿

二、 ⼆分答案

1 木材加⼯

2 砍树

3 跳石头


⼆分算法的原理以及模板其实是很简单的,主要的难点在于问题中的各种各样的细节问题。因此,⼤多数情况下,只是背会⼆分模板并不能解决题目,还要去处理各种乱七⼋糟的边界问题。

【案例】

题⽬来源: ⼒扣

题⽬链接: 34. 在排序数组中查找元素的第⼀个和最后⼀个位置

难度系数: ★★

【题⽬描述】

给你⼀个按照⾮递减顺序排列的整数数组 nums ,和⼀个⽬标值 target 。请你找出给定⽬标值

在数组中的开始位置和结束位置。

如果数组中不存在⽬标值 target ,返回 [-1, -1] 。

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题

【解法】

解法一:暴力解法->从前往后扫描数组(慢在没有利用数组有序的特征)

解法二:二分算法

解决时的细节问题(因题目而异):

1.while循环里面的判断如何写:while(left <right)/while(left <=right);

2.求中点的方法:(left + right)/2 / (left + right+1)/2;

3.二分结束后,相遇点的情况:需要判断一下,循环结束后,是否是我们想要的结果;

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int n = nums.size();
        //处理边界情况
        if(n == 0) return {-1,-1};
        //1.求起始位置
        int left = 0,right = n - 1;
        while(left < right)
        {
            int mid = (left + right) / 2;
            if(nums[mid] >= target) right = mid;
            else left = mid + 1;
        }
        if(nums[left] != target) return{-1,-1};
        int retleft = left;
        //2.求终止位置
        left = 0,right = n-1;
        while(left < right)
        {
            int mid = (left + right + 1) / 2;
            if(nums[mid] <= target) left = mid;
            else right = mid - 1;
        }
        return {retleft,left};
    }
};

【算法原理】

当我们的解具有⼆段性时,就可以使⽤⼆分算法找出答案:

  • 根据待查找区间的中点位置,分析答案会出现在哪⼀侧;
  • 接下来舍弃⼀半的待查找区间,转⽽在有答案的区间内继续使⽤⼆分算法查找结果

【模板】

⼆分的模板在⽹上⾄少能搜出来三个以上。但是,我们仅需掌握⼀个,并且⼀直使⽤下去即可。

// ⼆分查找区间左端点
int l = 1, r = n;
while(l < r)
{
    int mid = (l + r) / 2;
    if(check(mid)) r = mid;
    else l = mid + 1;
}
// ⼆分结束之后可能需要判断是否存在结果
/ ⼆分查找区间右端点
int l = 1, r = n;
while(l < r)
{
    int mid = (l + r + 1) / 2;
    if(check(mid)) l = mid;
    else r = mid - 1;
}
// ⼆分结束之后可能需要判断是否存在结果

 为了防止溢出,求中点时可以使用:mid = left + (right - left) / 2

【⼆分问题解决流程】

1. 先画图分析,确定使⽤左端点模板还是右端点模板,还是两者配合⼀起使⽤;

2. ⼆分出结果之后,不要忘记判断结果是否存在,⼆分问题细节众多,⼀定要分析全⾯。

【STL 中的⼆分查找】

<algorithm>

1. lower_bound :⼤于等于 x 的最⼩元素,返回的是迭代器;时间复杂度: O(log N) 。

2. upper_bound :⼤于 x 的最⼩元素,返回的是迭代器。时间复杂度: O(log N) 。

⼆者均采⽤⼆分实现。但是 STL 中的⼆分查找只能适⽤于"在有序的数组中查找",如果是⼆分答案就不能使⽤。因此还是需要记忆⼆分模板

一、⼆分查找例题

1 牛可乐和魔法封印

题⽬来源: ⽜客⽹

题⽬链接: ⽜可乐和魔法封印

难度系数: ★★

【解法】

解法一:暴力解法->从前往后扫描一遍(O(n*q))

解法二:二分算法

1.找到大于等于x的起始位置

2.找到小于等于y的终止位置

【参考代码】

#include<iostream>
using namespace std;
const int N = 1e5;
int a[N];
int n,q;
int main()
{
    cin >> n;
    for(int i = 1;i <= n;i++) cin >> a[i];
    cin >> q;
    while(q--)
    {
        int x,y;cin >> x >> y;
        //大于等于x的最小元素
        int left = 1,right = n;
        while(left < right)
        {
            int mid = (left + right) / 2;
            if(a[mid] >= x) right = mid;
            else left = mid + 1;
        }
        if(a[left] < x){
        	cout << 0 << endl;
        	continue;
		}
        int retleft = left;
        //小于等于y的最大元素
        left = 1,right = n;
        while(left < right)
        {
            int mid = (left + right + 1) / 2;
            if(a[mid] <= y) left = mid;
            else right = mid - 1;
        }
        if(a[left] > y)
        {
        	 cout << 0 << endl;
        	 continue;
		}
        cout << left - retleft + 1 << endl;
    }
    return 0;
}

    2 A-B 数对

    题⽬来源: 洛⾕

    题⽬链接:A-B 数对

    难度系数: ★

     【解法】

    分析可得到:

    性质一:元素的顺序是不影响最终结果的

    1. 可以先把整个数组排序
    2. 把A-B=C变形成B=A-C通过枚举A,然后查找有多少个B

    【参考代码】

    #include<iostream>
    #include<algorithm>
    using namespace std;
    
    typedef long long LL;
    const int N = 2e5 + 10;
    LL a[N];
    LL n,c;
    
    int main()
    {
    	cin >> n >> c;
    	for(int i = 1;i <= n;i++)cin >> a[i];
    	sort(a+1,a+n+1);
    	LL ret = 0;
    	for(int i = 2;i <=n;i++)
    	{
    		LL b = a[i] - c;
    		ret += upper_bound(a+1,a+i,b) - lower_bound(a+1,a+i,b);
    	}
    	cout << ret << endl;
    	return 0;
    }
    //其实此题的最优解是哈希表 题解如下:
    // #include<iostream>
    // #include<unordered_map>
    // using namespace std;
    
    // const int N = 2e5 + 10;
    // typedef long long LL;
    // LL a[N];
    // unordered_map<LL,LL> mp;
    // LL sum;
    // int main(){
    // 	LL n,c;cin >> n >> c;
    // 	for(LL i = 1;i <= n;i++){
    // 		cin >> a[i];
    // 		mp[a[i]]++;
    // 	}
    // 	for(int i = 1;i <= n;i++){
    // 		sum += mp[a[i] + c];
    // 	}
    // 	cout << sum << endl;
    // 	return 0;
    // }
    
    

    3 烦恼的高考志愿

    题⽬来源: 洛⾕

    题⽬链接: P1678 烦恼的⾼考志愿

    难度系数: ★★

    【解法】

    分析题意:给点一个数b,然后在数组中找出离b最近的那个数

    解法一:利用set来解决

    解法二:排序+二分(找出大于等于B的最小的元素的位置)

    【参考代码】

    #include<iostream>
    #include<algorithm>
    using namespace std;
    
    typedef long long LL;
    const int N = 1e5 + 10;
    LL a[N];
    int m,n;
    LL ret;
    int main()
    {
    	cin >> m >> n;
    	for(int i = 1;i <= m;i++) cin >> a[i];
    	sort(a,a+m+1);
    	a[0] = -1e7 + 10;
    	for(int i = 1;i <= n;i++)
    	{
    		LL x;cin >> x;
    		int left = 1,right = m;
    		while(left < right)
    		{
    			int mid = (left + right) / 2;
    			if(a[mid] >= x) right = mid;
    			else left = mid + 1;
    		}
    		ret += min(abs(a[left]-x),abs(a[left-1]-x));
    	}
    	cout << ret << endl;
    	return 0;
    }

    二、 ⼆分答案

    准确来说,应该叫做「⼆分答案 + 判断」。

    ⼆分答案可以处理⼤部分「最⼤值最⼩」以及「最⼩值最⼤」的问题。如果「解空间」在从⼩到⼤的「变化」过程中,「判断」答案的结果出现「⼆段性」,此时我们就可以「⼆分」这个「解空间」,通过「判断」,找出最优解。

    刚接触的时候,可能觉得这个「算法原理」很抽象。没关系, 道题的练习过后,你会发现这个「⼆分答案」的原理其实很容易理解,重点是如何去「判断」答案的可⾏性

    1 木材加⼯

    题⽬来源: 洛⾕

    题⽬链接: P2440 ⽊材加⼯

    难度系数: ★★

    【解法】

    解法一:暴力解法(这个解法的时间复杂度O(n*L)->1e13,会超时)

    • 枚举所有的切割长度x
    • 求出在x的情况下,能切出来多少段->C
    • 找出C>=k的情况下,最大的x

    解法二:利用二分来优化

    x表示:切割出来的小段长度

    c表示:在x的基础下,最多能切出来多少段

    k表示:最终要切出来的段数

    根据题意,可以发现如下性质,:

    • 当 x 增⼤的时候, c 在减⼩。也就是最终要切成的⻓度越⼤,能切的段数越少;
    •  当 x 减⼩的时候, c 在增⼤。也就是最终要切成的⻓度越⼩,能切的段数越多。

    那么在整个「解空间」里面,设最终的结果是 ret ,于是有:

    • 当x ≤ ret 时,c ≥ k 。也就是「要切的⻓度」⼩于等于「最优⻓度」的时候,最终切出来的段数「⼤于等于」k;
    • 当 x > ret时,c < k 。也就是「要切的⻓度」⼤于「最优⻓度」的时候,最终切出来的段数「⼩于」k ;

    在解空间中,根据 的位置,可以将解集分成两部分,具有「⼆段性」,那么我们就可以「⼆分答案」。

    当我们每次⼆分⼀个切成的⻓度 x 的时候,如何算出能切的段数 c ?

    • 很简单,遍历整个数组,针对每⼀根⽊头,能切成的段数就是 a[i] / x 。

    【参考代码】

    #include<iostream>
    using namespace std;
    
    typedef long long LL;
    const int N = 1e5 + 10;
    LL a[N];
    int n,k;
    
    //当切割长度为mid的时候,最多能切出来多少段 
    LL calc(int mid)
    {
    	LL c = 0;
    	for(int i = 1;i <= n;i++)
    	{
    		c += a[i] / mid;
    	}
    	return c;
    }
    int main()
    {
    	cin >> n >> k;
    	LL m = 0;
    	for(int i = 1;i <= n;i++)
    	{
    		cin >> a[i];
    		m = max(m,a[i]);
    	}
    	LL left = 0,right = m;
    	while(left < right)
    	{
    		LL mid = (left + right + 1) / 2;
    		if(calc(mid) >= k) left = mid;
    		else right = mid - 1;
    	}
    	cout << left << endl;
    	return 0;
    }

    2 砍树

    题⽬来源: 洛⾕

    题⽬链接:P1873 [COCI 2011/2012 #5] EKO / 砍树

    难度系数: ★★

    【解法】

    H表示:伐木机的高度

    C表示:当伐木机的高度为h的时候,所能获得的木材

    • 当 H 增⼤的时候, C 在减⼩;
    • 当 H 减⼩的时候, C 在增⼤。

    那么在整个「解空间」⾥⾯,设最终的结果是 ret ,于是有:

    当H<=ret 时 ,C>=M 。也就是「伐⽊机的⾼度」⼤于等于「最优⾼度」时,能得到的⽊材「⼩于等于」M ;

    当H>ret 时,C < M 。也就是「伐⽊机的⾼度」⼩于「最优⾼度」时,能得到的⽊材「⼤于」M 。

    在解空间中,根据 的位置,可以将解集分成两部分,具有「⼆段性」,那么我们就可以「⼆分答案」。
    当我们每次⼆分⼀个伐⽊机的⾼度 H 的时候,如何算出得到的⽊材 C ?

    • 很简单,遍历整个数组,针对每⼀根⽊头,能切成的⽊材就是 a[i] - H

    【参考代码】

    #include<iostream>
    using namespace std;
    typedef long long LL;
    const int N = 1e6 + 10;
    LL a[N];
    int n,m;
    
    // 当伐木机的高度为 x 时,所能获得的木材
    LL calc(int x)
    {
    	LL c = 0;
    	for(int i = 1;i <= n;i++)
    	{
    		if(a[i] > x) c += a[i] - x;
    	}
    	return c;
    }
    int main()
    {
    	cin >> n >> m;
    	LL maxh = 0;
    	for(int i = 1;i <= n;i++){
    		cin >> a[i];
    		maxh = max(maxh,a[i]);
    	}
    	LL left = 1,right = maxh;
    	while(left < right)
    	{
    		LL mid = (left + right + 1) / 2;
    		if(calc(mid) >= m) left = mid;
    		else right = mid - 1;
    	}
    	cout << left << endl;
    	return 0;
    }

    3 跳石头

    题⽬来源: 洛⾕

    题⽬链接: P2678 [NOIP2015 提⾼组] 跳⽯头

    难度系数: ★★

    【解法】

    x表示:最短的跳跃距离

    c表示:在跳跃距离为x的情况下,移走的岩石

    根据题意,我们可以发现如下性质:

    • 当 x 增⼤的时候, c 也在增⼤;
    • 当 x 减⼩的时候, c 也在减⼩。

    那么在整个「解空间」⾥⾯,设最终的结果是 ret ,于是有:

    • 当 时x ≤ ret,c ≤ M。也就是「每次跳的最短距离」⼩于等于「最优距离」时,移⾛的⽯头块数「⼩于等于」M ;
    • 当 时x > ret,c > M。也就是「每次跳的最短距离」⼤于「最优距离」时,移⾛的⽯头块数「⼤于」 M。

    在解空间中,根据ret 的位置,可以将解集分成两部分,具有「⼆段性」,那么我们就可以「⼆分答案」。

    当我们每次⼆分⼀个最短距离 x 时,如何算出移⾛的⽯头块数 c ?

    • 定义前后两个指针 i, j 遍历整个数组,设 i ≤ j ,每次 j 从 i 的位置开始向后移动;
    • 当第⼀次发现 a[j] - a[i] ≥ x 时,说明 [i + 1, j - 1] 之间的⽯头都可以移⾛;
    • 然后将 i 更新到 j 的位置,继续重复上⾯两步。

    【参考代码】

    #include<iostream>
    using namespace std;
    
    const int N = 5e4 + 10;
    int a[N];
    int l,n,m;
    //当最短跳跃距离为x时,移走的岩石数目 
    int calc(int x)
    {
    	int ret = 0;
    	for(int i = 0;i <= n;i++)
    	{
    		int j = i + 1;
    		while(j <= n && a[j] - a[i] < x) j++;
    		ret += j - i -1;
    		i = j - 1;
    	}
    	return ret;
    }
    int main()
    {
    	cin >> l >> n >> m;
    	for(int i = 1;i <= n;i++) cin >> a[i];
    	a[n+1] = l;
    	n++;
    	int left = 1,right = l;
    	while(left < right)
    	{
    		int mid = (left + right + 1) / 2;
    		if(calc(mid) <= m) left = mid;
    		else right = mid - 1;
    	}
    	cout << left << endl;
    	return 0;
     } 
    

    http://www.dtcms.com/a/122010.html

    相关文章:

  • STM32 vs ESP32:如何选择最适合你的单片机?
  • 网络协议学习
  • PDFtk
  • 2025年3月全国青少年软件编程等级考试(Python六级)试卷及答案
  • 带无源位置反馈气动V型调节开关球阀的特点解析-耀圣
  • find指令中使用正则表达式
  • C++中STL学习(一)——向量、栈、堆、集合
  • PyQt6实例_A股财报数据维护工具_解说并数据与完整代码分享
  • ISP的过程
  • 用户注册(阿里云手机验证码)
  • CNN(卷积神经网络)
  • 共工新闻社与韩国新华报社达成合作
  • Python | 第十四章 | 基于模块开发-出租系统
  • 如何设置 JVM 内存参数(-Xms、-Xmx、-Xss 等)?
  • 文件的操作
  • 自然语言处理入门6——RNN生成文本
  • 揭开 MCP 的神秘面纱:标准化 AI 上下文管理的未来(下)
  • 永磁同步电机 | 分类 / 转子结构 / FOC 控制 / 电路分析
  • Android 中集成 Unity 工程的步骤
  • 点云处理常用的软件、开源库及数据集
  • 将jar包制作成deb一键安装包
  • 从 Excel 到你的表格应用:条件格式功能的嵌入实践指南
  • 【机密计算顶会解读】13:CAGE:通过 GPU 扩展补充 Arm CCA
  • 2025年3月全国青少年软件编程等级考试(Python五级)试卷及答案
  • 图解Java设计模式
  • 005.Gitlab CICD变量使用
  • oauth2.0认证原理
  • word表格间隔设置
  • C++20 数学常数:<numbers> 头文件的革新
  • cmd清除网络共享连接凭证