算法精讲【整数二分】(实战教学)

算法精讲【整数二分】(实战教学)

---------------模板讲解---------------

前言:

🎯 文章的创作背景

本篇博客:算法精讲【整数二分】(实战教学) 是由博主听了B站up主: @一只会code的小金鱼的讲解后,那些卡住博主的 '死循环陷阱' 突然通了!然后博主开始奋笔疾书,通过结合up主小金鱼的讲解和自己的理解将这篇博客创作而来的


所以 :博主在这里向大家推荐这位up主😘@一只会code的小金鱼
温馨提示

  • 本片博客博主主要使用口语化的叙述,原因是:下面的内容大部分是博主的做题的心得体会(声明:"博主不是什么算法大佬,仅仅是一位 靠咖啡续命、靠玄学debug的算法咸鱼,分享这篇博客是为了记录自己的学习心得,如果能帮到同样挣扎的你,那就值回咖啡钱了!")
  • 这篇博客也是博主想到哪就写到哪完成的,所以会有写的不妥的地方,还请大佬指正
  • 博客中有一些二分的编程题练习并挂有链接,希望大家在熟悉了模板之后进行练习加以巩固
  • 每道编程题博主都有提供博主使用模板AC的代码,供大家参考(博主的代码中有大量的注释,记录了博主思考过程)
  • 写这篇博客主要是为了:
    • 教算法小白一个常用的算法:二分查找
    • 帮助算法初学者克服对二分的恐惧

在这里博主为正在学算法的你打打气就算AC率感人,我们也要倔强地WA下去!


学二分查找的痛点:二分查找的思想虽然简单,但是却极其容易写出死循环,那有有没有什么方法可以使我们又快又好的写出正确的二分查找呢?


深思 :出现这种情况的根本原因是二分虽然简单,但是二分的类型多,且不同种类型的二分的区别并不明显,这就使得我们常常容易搞混,面对具体的问题时一时不知道到底该怎么二分

这里博主将会根据自己的理解重新梳理所有的二分的情形,并尽量的简化模板,方便大家记忆。


在博主看来二分从形式上有以下四类:

虽然有四种情况,其实我们可以使用一个模板 来解决这四种情况

不说废话了,直接给大家上模板

温馨提示

  • C++代码的部分是直接可以照抄的部分
  • 伪代码的部分是需要我们根据具体的题意进行随机应变的部分

(下面有讲解应该怎么进行随机应变)

cpp 复制代码
//注意:在给binary_search函数传参时,区间端点(l,r)是开区间
//例如:你想对区间[1,9]进行二分查找,你应该传入的参数应该是:(l=0,r=10)
int binary_search(int l, int r, int 基准值)
{
	while (l + 1 < r)
	{
		int mid = (l + r) >> 1;
		if (待二分数组[mid] </<= 基准值)  l = mid;
		else  r = mid;
	}

	if (待二分数组[l/r] == 基准值) return l/r; //返回的l/r就是目标值的下标
    else return -1;
}

应用上面的模板进行二分查找中,只有两个地方需要我们具体情况具体分析:

随机应变1 :binary_search函数中l和r该谁进行更新if判断该怎么写

  • if (待二分数组[mid] 写<还是<= 基准值)

随机应变2 :binary_search函数中函数结束时该返回什么if判断该怎么写

  • if (q[写l还是r] == 基准值) return 返回l还是r;

疑问:如何熟练随机应变1?

第一步:要清楚分界线的位置在哪里?

第二步:是写<还是<=,就是根据分界线mid左侧的值和基准值之间的关系决定的

  • 基准值:简单一点说就是上面的图片中的5

<<=总共有2情况:

  • <:对应情况1和情况2
  • <=:对应情况3和情况4

所以:也就是说只要你可以知道这道题属于二分的哪种情况,随机应变1其实也可以照抄。


疑问:如何熟练随机应变2?

写l/r返回l/r组合在一起总共有4种情况:

  • 写r返回l:对应情况1
  • 写r返回r:对应情况2
  • 写l返回l:对应情况3
  • 写l返回r:对应情况4

所以:也就是说只要你可以知道这道题属于哪种情况,随机应变2其实也可以照抄。

说的有点抽象,我们直接看图片演示 + 代码示例:

情况1:找到最后一个<3的元素的位置

cpp 复制代码
int binary_search(int l, int r, int target)
{
	while (l + 1 < r)
	{
		int mid = (l + r) >> 1;
		if (q[mid] < 3)  l = mid; //随机应变1:写<
		else  r = mid;
	}

	if (q[r] == 3) return l;// 随机应变2:写r ,返回l
	else return -1;
}

情况2:找到第一个>=3的元素的位置

cpp 复制代码
int binary_search(int l, int r, int target)
{
	while (l + 1 < r)
	{
		int mid = (l + r) >> 1;
		if (q[mid] < 3)  l = mid; //随机应变1:写<
		else  r = mid;
	}

	if (q[r] == 3) return r;// 随机应变2:写r ,返回r
	else return -1;
}

情况3:找到最后一个<=3的元素的位置

cpp 复制代码
int binary_search(int l, int r, int target)
{
	while (l + 1 < r)
	{
		int mid = (l + r) >> 1;
		if (q[mid] <= 3)  l = mid; //随机应变1:写<=
		else  r = mid;
	}

	if (q[l] == 3) return l;// 随机应变2:写l ,返回l
	else return -1;
}

情况4:找到第一个>3的元素的位置

cpp 复制代码
int binary_search(int l, int r, int target)
{
	while (l + 1 < r)
	{
		int mid = (l + r) >> 1;
		if (q[mid] <= 3)  l = mid; //随机应变1:写<=
		else  r = mid;
	}

	if (q[l] == 3) return r;// 随机应变2:写l ,返回r
	else return -1;
}

---------------经典例题---------------

入门经典例题:789.数的范围

开始挑战: 789.数的范围

如果你已经掌握了上面我讲的内容的话,那么我们不难看出这道问题所属的二分类型:

  • 所求元素的起始位置 ---> 第2种情况
  • 所求元素的终止位置 ---> 第3种情况
cpp 复制代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N = 100010;  
int n, q;
int arr[N];  


//第一个二分查找找的是:被询问数的第一次出现的位置(下标)
int binary_search1(int arr[], int len, int x) {
    int l = -1, r = len;
    while(l + 1 < r) 
    {
        int mid = (l + r) / 2;
        if(arr[mid]<x)  l = mid;
        else  r = mid;
    }
    if(arr[r] == x) return r;
    else return -1;  //找不到就返回-1
}


//第二个二分查找找的是:被询问数的最后一次出现的位置(下标)
int binary_search2(int arr[], int len, int x) {
    int l = -1, r = len;
    while(l + 1 < r) 
    {
        int mid = (l + r) / 2;
        if(arr[mid]<= x)  l = mid;
        else  r = mid;
    }
    if(arr[l] == x) return l;
    else return -1;  //找不到就返回-1
}

int main() 
{
    scanf("%d %d", &n, &q);
    for(int i = 0; i < n; i++)  scanf("%d", &arr[i]);

    while(q --) 
    {
        int x;
        scanf("%d", &x);
        
        int res1 = binary_search1(arr, n, x);
        int res2 = binary_search2(arr, n, x);
        
        printf("%d %d\n", res1, res2);
    }
    return 0;
}    

总结

虽然我们的方法并不高明,甚至在别人看来是愚蠢的写法,为了写这道题还要两个二分查找函数。

但是我们的方法胜在模板固定,仅需稍作修改每种二分情况便都能使用,并且修改的判断也很简单。

根本不需要动脑分析什么边界情况,更不会出现死循环的情况,彻底解放🧠

---------------模板练习---------------

强化训练题单:洛谷【算法1-6】二分查找与二分答案

P2249 【深基13.例1】查找

题目介绍

开始挑战: P2249 【深基13.例1】查找

方法一:

如果你已经熟练的掌握了上面的模板,同样我们不难看出来这道题就是二分的第2种情况

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

//获取数据:
//1.存储整数的个数 ---> 一个int变量
//2.存储询问的个数 ---> 一个int变量
//3.存储所有的整数 ---> 一个一维数组
int n,m;
const int N=1e6+10;
vector<int> arr(N);

//实现二分查找函数
//要求输出这个数字在序列中第一次出现的编号 ---> 找到第一个>=编号的元素的位置
int binary_search(int l,int r,int a)
{
    while(l+1<r)
    {
      int mid=(l+r)>>1;
      if(arr[mid]<a) l=mid;
      else r=mid;
    }
    if(arr[r]==a) return r;
    else return -1;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>arr[i];

    //处理数据:
    while(m--)
    {
        int a;
        cin>>a;
        int ret =binary_search(0,n+1,a);//注意:这里的n+1可千万别写成了arr.size()+1
        //因为arr.size() 返回的是 N=1e6+10(固定容量) ---> 导致搜索范围过大,可能访问未初始化的内存
        cout<<ret<<" ";
    }
    
}

P1102 A-B 数对

题目介绍

这道题稍微绕了一点弯,但是如果你能看出来这道其实就是:二分的第2种情况 + 二分的第3种情况

那这道题也是很简单啦

开始挑战: P1102 A-B 数对

方法一:

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

//获取数据:
//1.记录整数的个数 ---> 一个int变量
//2.记录整数A - 整数B的结果 ---> 一个int变量
int n, m;
//3.存储整数的信息 ---> 一个一维数组
const int N = 2e5 + 10;
int arr[N];
//4.记录最终的结果 --> 一个long long变量
long long ret = 0;//一定记得使用long long类型存储最后的结果

//通过观察是:二分查找的情况2 + 情况3
//实现二分查找函数情况1
int binary_search1(int l, int r, int x)
{
    while (l + 1 < r)
    {
        int mid = l + r >> 1;
        if (arr[mid] < x) l = mid;
        else r = mid;
    }

    if (arr[r] == x) return r;
    else return -1;
}

//实现二分查找函数情况2
int binary_search2(int l, int r, int x)
{
    while (l + 1 < r)
    {
        int mid = l + r >> 1;
        if (arr[mid] <= x) l = mid;
        else r = mid;
    }

    if (arr[l] == x) return l;
    else return -1;
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> arr[i];

    sort(arr, arr + n);//注意:这道并没有保证输入数据是有序的

    //处理数据:
    for (int i = 0; i < n; i++)
    {
        int res1 = binary_search1(-1, n, arr[i] + m);
        int res2 = binary_search2(-1, n, arr[i] + m);

        if (res1 != -1)//找的了满足条件的数字
        {
            if (res1 == res2) ret++; //满足条件的数字只有一个
            else ret += (res2 - res1 + 1);//满足条件的数字有多个
        }
    }

    //输出数据:
    cout << ret;
    return 0;
}

注意

  • 这里的arr数组不要使用vector数组,要使用原生数组,原因以下两点:

    1. 使用vector数组不能单纯的写成sort(arr.begin(),arr.end()),这样话sort函数会排序2e5 + 10个元素,需要手动限制排序的元素区间为输入整数的范围
    2. vector数组的数组名并不可以像原生数组那样直接+ - 一个数字来移动,需要在迭代器的基础上再 + - 数字进行移动
  • 这里记录结果的cnt变量的类型需要是long long类型

P1678 烦恼的高考志愿

题目介绍

开始挑战: P1678 烦恼的高考志愿

方法一:

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

//获取数据:
//1.记录学校的数量 --> 一个int变量
//2.记录学生的数量 --> 一个int变量
int m,n;
//3.存储学校的预计分数线 ---> 一个一维数组
//4.存储每位学生的估计分数 --> 一个一维数组
const int N=1e5+10;
int school[N],student[N];

//实现二分查找函数:
int binary_search(int l,int r,int x)
{
  while(l+1<r)
  {
    int mid=l+r>>1;
    if(school[mid]<x) l=mid;
    else r=mid;
  }
  return min(abs(x-school[l]),abs(x-school[r]));
}


int main()
{
  cin>>m>>n;
  for(int i=0;i<m;i++) cin>>school[i];
  for(int i=0;i<n;i++) cin>>student[i];

  //处理数据:
  sort(school,school+m);
  
  long long cnt=0;
  for(int i=0;i<n;i++) //枚举每位同学的成绩
  {
    if(student[i]<=school[0]) cnt+=school[0]-student[i];
    else cnt+=binary_search(-1,m,student[i]);
  }
  //输出数据:
  cout<<cnt;
  return 0;
}

---------------模板强化---------------

上面我们刷的题都是一些很浅显的让我们在一个范围很大的区间中找一个满足要求的数字,只要我们可以熟练的使用模板,都是洒洒水而已,根本不需要动脑思考(或许需要转一下弯)

接下来我们要强化训练一些有难度的题,也就是情景有点复杂的问题

难点体现在:

  1. 我们或许根本就看不出来这道题要使用二分查找(在你事先并不知道我们在进行二分查找的专项训练时)
  2. 即使你发现要使用二分查找来作,但是也不能灵活使用上模板(重点突破
    面对这种情况我们需要回到二分查找的最原始的模板

因为其实之前给大家展示的模板是经过简化的模板,仅仅适用于在一个范围很大的数字区间中找一个满足要求的数字。


疑问:为什么之前的模板解决这个适用范围很窄?

  • 这是因为之前的模板中的if (待二分数组[mid] </<= 基准值)这条语句让我们写死了。
  • 其实原始模板中if语句中的条件是复杂多变的,常常我们要为此专门写一个 check函数 来进行判断是:更新l还是更新r

所以:大家万不可认为二分查找只有在一个范围很大的数字区间中找一个满足要求的数字的时候才会使用到(不要先入为主,限制了自己的思维),其实二分是一种思想它能应用的场景远不止这些,二分远比我们想像中的应用价值大:

  1. 二分的应用场景广,上场次数多非常实用,通常我们使用双重for循环暴力解题导致的超时,其中的一个for循环都可以使用二分优化掉,所以如果你会暴力解这道题,再配合二分的话,就可以AC
  2. 它的时间复杂度为 O ( l o g n ) O(logn) O(logn),熟练的在多种场景下使用它可以使我们的代码的时间复杂度大幅度的下降。
    强化后的模板会诞生两个模板:(这里先告诉大家什么情景下使用模板1,什么情况下使用模板2)
  • 模板1的使用情景 :问题想要的东西 越大越好
  • 模板1的使用情景 :问题想要的东西 越小越好

(你也可以看看下面的习题是不是符合上面的规则,都是符合的,我已经帮你们看过了)

强化模板1

cpp 复制代码
bool check(int x) 
{
    /*
    	这里我省略了很多的代码
    	或者根本没有代码
    	需要我们根据题意进行随机的应变
    	......
    */
    if(随机应变) return true;
    else return false;
}

int binary_search(int l, int r)
{
	while (l + 1 < r)
	{
		int mid = (l + r) >> 1; 
		if (check(mid))  l = mid;
		else  r = mid;
	}
    
    if(check(r)) return r;
    else return l;
}

//记忆小妙招:我们想要的结果在左侧

强化模板2

cpp 复制代码
bool check(int x) 
{
    /*
    	这里我省略了很多的代码
    	或者根本没有代码
    	需要我们根据题意进行随机的应变
    	......
    */
    if(随机应变) return true;
    else return false;
}

int binary_search(int l, int r)
{
	while (l + 1 < r)
	{
		int mid = (l + r) >> 1; 
		if (check(mid))  r = mid;
		else  l = mid;
	}
    
    if(check(l)) return l;
    else return r;
}

//记忆小妙招:我们想要的结果在右侧

模板1和模板2的区别有那些?

  1. 二分函数中check返回值为true时更新为mid的变量不同

    • 模板1:if (check(mid)) l = mid;
    • 模板2:if (check(mid)) r = mid;
  2. 二分函数中最后的返回的依据不同

    • 模板1:if(check(r)) return r;
    • 模板2:if(check(l)) return l;
      对于原始模板我们要转变一些之前模板带给我们的影响:
  3. mid:代表我们要找的目标值在数组中的下标 ---> 代表我们要找的目标值

  4. l和r:代表我们要找的目标值所在数组的左边界右边界 ---> 代表目标值可能取值的最小值最大值


第一步 :所以这里我们就引出了熟练掌握原始模板的第一步:我们要先能知道我们要找的目标值可能取的最小值和最大值分别是多少?

第二步 :那我不说大家都能猜到第二步:怎么实现这个check函数 ?

  1. 首先明确这个check函数的返回值是bool类型的

  2. 其次就是明确它check的对象是你用二分新找到的可能是答案的值mid

  3. 最后要明确这个函数内部本质上就是一个if判断语句(核心)

    cpp 复制代码
    //伪代码进行说明:
    if(新找到的值mid是我们想要找的目标值) return true;
    else return false;
       
    //我想表达的意思就是check函数中具体有什么我不清楚,
    //但是我明白一点:函数中一定有if语句,并且它的形式就是上面的形式
    • if语句中的内容是需要大家根据题意按要求写出的合理判断(这个真的要靠大家随机应变了,一般如果你会暴力解这道题的话,check函数你也是可以写出来)

第三步 :最后我们再回到binary_search函数中来完成第三步:怎么决定最终binary_search函数要返回l还是r呢?

cpp 复制代码
//模板1:问题想要的东西越大越好
if (check(r)) return r;
else return l;

//模板2:问题想要的东西越小越好

---------------强化例题---------------

P1873 [COCI 2011/2012 #5] EKO / 砍树

题目介绍

开始挑战: P1873 [COCI 2011/2012 #5] EKO / 砍树

方法一:

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

//1.记录树木的数量 --->  一个int变量
//2.记录需要的树木的长度 ---> 一个int变量
//3.存储树木的长度的信息 ---> 一个一维数组
int n;
long long m;// 这里要注意需要的树木的长度的最大范围超过了int变量的最大范围,这里要使用long long类型
const int N = 1e6 + 10;
int arr[N];


//实现check函数
bool check(int x)
{
    int sum = 0;
    for (int i = 0; i < n; i++)
    {
        sum += max(0, arr[i] - x);
        if (sum >= m) return true;
    }
    return false;

}

//实现二分查找函数:
//1.明确我要查找的锯片的高度的取值范围:[0,4e5]
//2.再去实现check函数
int binary_search(int l, int r)
{
    while (l + 1 < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) l = mid;
        else r = mid;
    }
    if (check(r)) return r;
    else return l;
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> arr[i];

    //处理数据:
    int ret=binary_search(-1,4e5+1);

    //输出数据:
    cout << ret;
    return 0;
}

这里我就依据上面我说的三步骤来来解决这道题:
第一步我们要先能知道我们要找的目标值的可能取的最小值和最大值分别是多少?

  • 目标值:一个合适伐木锯片的高度
  • 范围:它取值的范围也就是树的高度的取值范围[0,4e5] ---> 所以l=-1,r=4e5+1

第二步怎么实现这个check函数 ?

  1. 首先我们要去想发生什么样的情况时,我们二分新找到的值mid是符合要求的 --> 回答:当我们用长度为mid的锯片获得的木材的长度 >= 我们需要的木材的长度m时
    • 经过上面的分析我们可以看出来,这时我们还不能直接进行if判断,因为我们还没有锯片的长度为mid时获得的木材的长度
  2. 同时经过上面的分析我们也可以发现if语句中判断条件就是sum >= m

第三步怎么决定最终binary_search函数要返回l还是r呢?

cpp 复制代码
//锯片的高度越大越好 ---> 这道题使用模板1
if (check(r)) return r;
else return l;

---------------强化训练---------------

这里的强化训练主要强化的是:大家在面对一个二分问题时,可以快速反应出这道题的check函数应该怎么写的能力。

也就是帮助大家搞定二分查找算法中唯一的需要进行随机应变的那部分应该怎么写。

我们先来看一看check函数的核心:我们新获得的二分值是符合题目的要求的?

  • 我相信大家都已经明白了check的对象是:我们二分出来的值 ---> 可能的目标值 ---> 通俗点说:"答案的可能值"
  • 但是面对不同的题大家还不太清楚:什么时候算是check成功了
    • 就拿示例来说:上面的砍树这道题 ---> 一眼可以看出来"当我们砍出的木材的长度>=我们需要的木材的长度"时就算是check成功了
    • 但是拿下面的这道木材加工问题来说:
      • 我们第一眼可能就看不出来了(没有思路,出现想到用二分但是写不出来的问题)
      • 或者说我们将check成功的条件想成是 ---> 小段的长度x * 小段的数量k <= 原木的长度(点评:这里我们想到了去二分"答案:小段的长度",但是我们的check条件是错误,虽然它看上去是那么的合理)
      • 而这道题正确的check条件是:每颗原木的长度arr[i] / 当小段木头的长度x 的和 >= 小段木头的数量k

根据博主的经验:可以从下面的一个角度进行思考来保证我们的check函数的条件一定是正确的

一般不等号的右边都是问题中的一个要求限制,例如:

  • 砍树:木材的长度必须>=要求的 m(限制条件)
  • 加工:小段的数量必须>=要求的 l(限制条件)

P2440 木材加工

题目介绍

开始挑战: P2440 木材加工

方法一:

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

//获取数据:
//1.原木的数量 --> 一个int变量
//2.需要的小段的数量 --> 一个int变量
int n, k;
//3.存储所有原木的长度信息 ---> 一个一维数组
const int N = 1e5 + 10;
int arr[N];

//实现check函数
bool check(int x)
{
    //思考:什么时候二分出来的值是符合要求的呢?---> 回答:每颗原木的长度 / 当小段木头的长度x 的和 >= 小段木头的数量k 
    //缺少:"每颗原木的长度 / 当小段木头的长度x 的和"
    int sum = 0;
    for (int i = 0; i < n; i++)
    {
        sum += arr[i] / x;
        if (sum >= k) return true;
    }
    return false;
}

//实现二分查找函数
//1.首先明确目标值的可能存在的范围,即:小段木头的范围[1,1e8]
//2.实现check函数
int binary_search(int l, int r)
{
    while (l + 1 < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) l = mid;
        else r = mid;
    }

    if (check(r)) return r;
    else return l;
}

int main()
{
    cin >> n >> k;
    for (int i = 0; i < n; i++) cin >> arr[i];


    //处理数据:
    int ret = binary_search(0, 1e8 + 1);

    //处理数据:
    cout << ret;
    return 0;
}

---------------进阶试炼---------------

当你已经完全的掌握了原始的二分查找的模板,并且也能快速的反应出check函数中的if判断语句该怎么写的话,那么你可以尝试进行进阶试炼啦。

进阶试炼中的问题的难点在于:

虽然你可以快速的反应出check函数中的if判断语句该怎么写,但是往往你并不能很容易 "使用新二分出来的值来处理出if判断需要的值"

说的通俗易懂一点就是:if(新二分的值处理出的值 不等式 要求限制的值)中不等式左侧的值你往往不知道怎么得到

所以进阶试炼的目的就是训练我们的这个能力的,并且这个能力是只有我们通过刷题训练才能得到的。(没有套路,每道题的情景都不一样,需要我们一点一点不断的积累)

P2678 [NOIP 2015 提高组] 跳石头

题目介绍

开始挑战: P2678 [NOIP 2015 提高组] 跳石头

方法一:

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

//获取数据:
//1.起点到终点的距离 ---> 一个long long变量
//2.石头的数量 --> 一个int变量
//3.可以搬走的石头的最大数量 --> 一个int变量
long long L;
int n, m;
//4.存储每块石头到起点的距离 ---> 一个一维数组
const int N = 5e4 + 10;
int arr[N];

//实现check函数
bool check(int x)
{
    //思考:何时我们新二分出来的目标值符合我们的要求 ---> 回答:当我们为了满足题意需要移走石头的数量<=组委会可以移动走到石头的数量m
    //深思:需要满足什么题意:每两个相邻石头之间的距离arr[i]-arr[last] < 新二分出来的值(最短的跳跃距离)

    //1.所以先统计出:要实现任意两个石头之间的距离都大于新二分出来的值(最短的跳跃距离)的话,应该移动多少的石头
    int cnt = 0;
    int last = 0;
    for (int i = 1; i <= n + 1; i++)
    {
        if (arr[i] - arr[last] < x) cnt++;
        else last = i;
    }
    if (cnt <= m) return true;
    else return false;
}

//实现二分查找函数
//1.首先要明确我们要查找的目标值可能取值的范围,即:跳跃距离中的最小值的范围[1,1e9]
//2.实现check函数
int binary_search(int l, int r)
{
    while (l + 1 < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) l = mid;
        else r = mid;
    }

    if (check(r)) return r;
    else return l;
}
int main()
{
    cin >> L >> n >> m;
    for (int i = 1; i <= n; i++) cin >> arr[i];
    arr[n + 1] = L;
    //处理数据:
    int ret = binary_search(0, 1e9 + 1);

    //输出数据:
    cout << ret;
    return 0;
}

P3853 [TJOI2007] 路标设置

通过下面的这道题我们更能体会到这句话的含义:

虽然你可以快速的反应出check函数中的if判断语句怎么写,往往你并不能很容易 "使用新二分出来的值来处理出if判断需要的值"

题目介绍

开始挑战: P3853 [TJOI2007] 路标设置

方法一:

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

//获取数据:
//1.公路的长度 ---> 一个int变量
//2.原有的路灯的数量 --> 一个int变量
//3.可以新增路灯的数量 ---> 一个int变量
int L, n, k;
//4.存储原有的路灯距离起点的距离 ---> 一个一维数组
const int N = 2e5 + 10;
int arr[N];


//实现check函数
bool check(int x)
{
    //思考:二分新查找的值何时才算做是满足了条件可以成功退出check函数?---> 回答:在这种空旷指数的情况下对应的新增的路灯的数量 <= 要求的新增路灯的数量
    //深思:在这种空旷指数的情况下对应的新增的路灯的数量 "怎么去求?" ---> 回答:如果某两个路灯之间的空旷值 > 二分新查找的值 --> 我们应该增设路灯的数量
    int cnt = 0, last = 0;
    for (int i = 1; i <= n + 1; i++)
    {
        if (arr[i] - arr[last] > x)
        {
            cnt++;
            int num = arr[i] - arr[last] - x;
            while (num > x)
            {
                cnt++;
                num -= x;
            }
        }
        last = i;
    }

    if (cnt <= k) return true;
    else return false;
}
//实现二分查找函数
//1.首先要明确:目标值可能出现的范围,即:公路的空旷指数的可能的取值范围[0,1e7]
//2.实现check函数
int binary_search(int l, int r)
{
    while (l + 1 < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid;
    }
	//公路的空旷指数越小越好 ---> 这道题使用模板2
    if (check(l)) return l;
    else return r;
}
int main()
{
    cin >> L >> n >> k;
    for (int i = 1; i <= n; i++)
    {
        cin >> arr[i];
    }
    arr[n + 1] = L;

    //处理数据:
    int ret = binary_search(-1, 1e7 + 1);

    //输出数据:
    cout << ret;
    return 0;
}

P1182 数列分段 Section II

题目介绍

开始挑战: P1182 数列分段 Section II

方法一:

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

//获取数据:
//1.记录整数数列的长度 --> 一个int变量
//2.记录需要分的段数 --> 一个int变量
int n,m;
//3.存储整数数列的信息 ---> 一个一维数组(整数的数量)
const int N=1e5+10;
int arr[N];

//实现check函数
int check (int x)
{
  //思考:什么时候我们新二分出来的值是符合要求的 --->回答:当我们要使新二分出来的每段和的最大值成为可能的话需要划分的段数 = 题目需要分出来的段数
  //深思:怎么求出:"当新二分出来的段和是最大值时,需要划分的段数"
  int cnt=0,sum=0;
  for(int i=0;i<n;i++) //枚举数列中的每一个元素
  {
    if(sum+arr[i]<=x) sum+=arr[i];
    else 
    {
      cnt++;
      sum=arr[i];
    }
  }
  if(sum) cnt++; //处理最后一段
  
  if(cnt<=m) return true;
  else return false;
}

//实现二分查找函数:
//1.首先要明确目标值的可能的取值范围,即:每段和的取值范围[数列中的最大值,数列中所有元素的和]
//2.实现check函数
int binary_search(int l,int r)
{
  while(l+1<r)
  {
    int mid=l+r>>1;
    if(check(mid)) r=mid;
    else l=mid;
  }
  //每段和的最大值越小越好 ---> 这道题使用模板2
  if(check(l)) return l;
  else return r;
}

int main()
{
  cin>>n>>m;
  int maxx=0,summ=0;
  for(int i=0;i<n;i++)
  {
      cin>>arr[i];
      maxx=max(maxx,arr[i]);
      summ+=arr[i];
  }

  //处理数据:
  int ret=binary_search(maxx,summ);
  
  //输出数据:
  cout<<ret;
  return 0;
}

代码片段解释

cpp 复制代码
maxx = max(maxx, arr[i]);
summ += arr[i];

int ret=binary_search(maxx,summ);

1. maxx的作用 - 确定左边界

  • 物理意义 :当分段数m等于数组长度n时(即:每个元素单独成段),此时最大段和就是数组中的最大元素值。
  • 算法意义 :这是解的最小可能值,作为二分查找的左边界。
    • 示例:数组[4,2,4,5,1]maxx=5,若分5段,最大和就是5

2. summ的作用 - 确定右边界

  • 物理意义:当只分1段时(即:整个数组作为一段),此时最大段和就是数组总和。
  • 算法意义 :这是解的最大可能值,作为二分查找的右边界。
    • 示例:上述数组summ=16,若分1段,最大和就是16

疑问:为什么要使用[maxx,summ]作为二分的区间范围,而不是范围[1,1e9]呢?

如果不计算maxx:(必须要,不然只能跑过4/5的样例)

  • 若将左边界设为0,当检查x=3时:
    • 遇到元素5时必然失败(因为5>3)
    • 但实际解可能是5(当m=n时)
    • 会导致错误判断

如果不计算summ:(非必须,但是可以带来优化)

  • 右边界过大(如1e9)会导致:
    • 不必要的二分迭代
    • 可能整数溢出风险
相关推荐
天下琴川33 分钟前
Dify智能体平台源码二次开发笔记(5) - 多租户的SAAS版实现(2)
人工智能·笔记
序属秋秋秋1 小时前
算法基础_数据结构【单链表 + 双链表 + 栈 + 队列 + 单调栈 + 单调队列】
c语言·数据结构·c++·算法
apcipot_rain2 小时前
【密码学——基础理论与应用】李子臣编著 第五章 序列密码 课后习题
算法·密码学
不要不开心了2 小时前
sparkcore编程算子
pytorch·分布式·算法·pygame
88号技师2 小时前
【2024年最新IEEE Trans】模糊斜率熵Fuzzy Slope entropy及5种多尺度,应用于状态识别、故障诊断!
人工智能·算法·matlab·时序分析·故障诊断·信息熵·特征提取
清同趣科研2 小时前
R绘图|6种NMDS(非度量多维分析)绘图保姆级模板——NMDS从原理到绘图,看师兄这篇教程就够了
人工智能·算法
workworkwork勤劳又勇敢2 小时前
Adversarial Attack对抗攻击--李宏毅机器学习笔记
人工智能·笔记·深度学习·机器学习
杜小暑3 小时前
冒泡排序与回调函数——qsort
c语言·算法·排序算法
徵6863 小时前
代码训练day27贪心算法p1
算法·贪心算法
寻丶幽风4 小时前
论文阅读笔记——Generating Long Sequences with Sparse Transformers
论文阅读·笔记·语言模型·transformer·稀疏自注意力