欢迎来到我的频道[【点击跳转专栏】]
作者说:我想说 基础 不等于 简单 ;算法能力不是一蹴而就的,而是来自日积月累的积累和练习!积小流终成江海,诸君 加油!!
文章目录
- [1. 二分算法](#1. 二分算法)
- [1.1 案例:在排序数组中查找元素的第⼀个和最后⼀个位置(超级详细~)](#1.1 案例:在排序数组中查找元素的第⼀个和最后⼀个位置(超级详细~))
- [1.2 模版总结](#1.2 模版总结)
- [1.3 二分算法的时间复杂度](#1.3 二分算法的时间复杂度)
- [2. ⼆分查找](#2. ⼆分查找)
- [2.1 ⽜可乐和魔法封印](#2.1 ⽜可乐和魔法封印)
- [2.2 A-B 数对](#2.2 A-B 数对)
- [2.3 烦恼的高考志愿](#2.3 烦恼的高考志愿)
- [3. 二分答案](#3. 二分答案)
- [3.1 木材加工(二分答案模版题)](#3.1 木材加工(二分答案模版题))
- [3.2 砍树](#3.2 砍树)
- [3.3 跳石头(挺难的 难在找二分性~)](#3.3 跳石头(挺难的 难在找二分性~))
1. 二分算法
二分算法是基础算法中最难的算法。二分算法的原理以及模板其实是很简单的,主要的难点在于问题中的各种各样的细节问题。因此,大多数情况下,只是背会二分模板并不能解决题目,还要去处理各种乱七八糟的边界问题。
1.1 案例:在排序数组中查找元素的第⼀个和最后⼀个位置(超级详细~)
https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/description/
解法:二分算法 契机:发现解集中,存在二段性当我们的解具有二段性时,就可以使用二分算法找出答案:
- 根据待查找区间的中点位置,分析答案会出现在哪一侧;
- 接下来舍弃一半的待查找区间,转而在有答案的区间内继续使用二分算法查找结果。
如:定义两个指针
left 和 right分别指向解空间的起始位置和终止位置!然后再求一下中点位置mid,假如我要寻找的是2mid指向3那么此时我们就可以舍弃掉【mid,right】这个区间的值!下面我会很细节的讲解该如何找到
2的起始位置(3号位置的2)和2的终止位置(5号位置的2)
查找起始位置:
结合上面的图 我们发现初始位置可以划分为
小于2 和大于等于2这两个部分关于这个特性,起始位置二分算法接下来如何操作(
答案在大于等于2的部分 这么做可以准确找到这个点 可以试着自己画画),如图:
细节问题:
- while循环里面的判断怎么写?
不可以用while(left<=right)会导致死循环!
此时 命中第一个条件
a[mid]>=2然后right==mid此时mid=(0+0)/2=0你会发现直接死循环了!
while循环要写成while(left<right)这样我们会发现 当变成如图的时候 就已经跳出循环,并且该结果就是我们所要的最终结果!
- 求中点的方式
方式1:
(left+right)/2方式2:
(left+right+1)/2
(当总数为奇数的时候 两种方式都是求得 正中间的那一个!)
(但当为偶数的时候 两种求法的方式就变了!)
我们要用
方式1而不是方式2因为(left+right+1)/2这种方式可能造成死循环!
此时我们命中 a[mid]>=t这个操作 此时mid=right又陷入死循环!当我们使用
方式一的话就不会造成死循环 具体我就不画了 可以自己画一画试试!
- 二分结束后,相遇点会是何种情况?
循环结束后 我们需要判断一下,是否是我们想要的结果!比如我们要找
2但是如图按照我们上述规则进行循环结束后 并没有我们想要的值。
查找终止位置:
结合上面的图 我们发现初始位置可以划分为
小于等于2 和大于2这两个部分关于这个特性,起始位置二分算法接下来如何操作,如图:
细节问题(这一块类比一下
求初始位置的方式可以自己举个例子看看 我就不详细分析了):
- 求中点的方式
方式1:
(left+right)/2❌ 会死循环
这么做会命中
条件1 a[mid]<=t此时left=mid死循环了!
如果是第二种方式
(left+right+1)/2就不会造成死循环!
此时
mid==right满足条件1a[mid]<=t此时letf=mid就不会造成死循环了(结果如下图)
- 接上面例子 while该怎么写?
如果
while(left<=right)看上图 死循环了 不想多解释 所以必须要while(left<right)
- 最后老规矩 判断一下相遇点的情况就行了!(
其实不写也没事 因为起始点已经判断出有没有了)
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target)
{
int n=nums.size();
//1.求起始位置
//这里发生了隐式类型转换。n 是 size_t(无符号整数 vector.size()的返回类型),0 - 1 不会变成 -1,而是变成了一个巨大的正整数
//虽然变量 n 是 int,但在计算 n - 1 时,编译器为了"安全",偷偷把 n 又变回了 size_t这个值 也就是"类型穿透"现象
//然后当你访问num[mid]就会越界!因为此时nums是空数组!
//记得要处理边界情况
if(n==0) return{-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};
}
};
1.2 模版总结
cpp
// ⼆分查找区间左端点
int l = 1, r = n;
while(l < r)
{
int mid = (l + r) / 2;
//check表示 如果左端点[left,mid]这个区间
if(check(mid)) r = mid;
else l = mid + 1;
}
// ⼆分结束之后可能需要判断是否存在结果
cpp
// ⼆分查找区间右端点
int l = 1, r = n;
while(l < r)
{
int mid = (l + r + 1) / 2;
//check表示 如果右端点在[mid,right]这个区间
if(check(mid)) l = mid;
else r = mid - 1;
}
// ⼆分结束之后可能需要判断是否存在结果
在工程代码中,为了防止 left + right 溢出(虽然 int 很难溢出,但在极大数组下可能),更严谨的写法通常是
cpp
int mid = left + (right - left) / 2;
【模板记忆方式】
- 不用死记硬背,算法原理搞清楚之后,在分析题目的时候自然而然就知道要怎么写二分的代码;
- 仅需记住一点,
if/else中出现-1的时候,求mid就+1就够了。
【二分问题解决流程】
- 先画图分析,确定使用左端点模板还是右端点模板,还是两者配合一起使用;
- 二分出结果之后,不要忘记判断结果是否存在,二分问题细节众多,一定要分析全面。
【STL 中的二分查找】
<algorithm>
lower_bound:大于等于 x 的最小元素,返回的是迭代器;时间复杂度: O ( log N ) O(\log N) O(logN)。upper_bound:大于 x 的最小元素,返回的是迭代器。时间复杂度: O ( log N ) O(\log N) O(logN)。
二者均采用二分实现。但是 STL 中的二分查找只能适用于"在有序的数组中查找",如果是二分答案就不能使用。因此还是需要记忆二分模板。
cpp
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target)
{
// 1. 特判空数组(依然需要,防止后面 nums[0] 越界)
if(nums.empty()) return {-1, -1};
// 2. 找左边界:第一个 >= target 的位置
// lower_bound 返回的是迭代器,减去 begin() 得到下标
int left = lower_bound(nums.begin(), nums.end(), target) - nums.begin();
// 3. 找右边界:
// 技巧:找第一个 > target 的位置,然后下标 - 1
int right = upper_bound(nums.begin(), nums.end(), target) - nums.begin() - 1;
// 4. 检查找到的范围是否合法
// 如果 left 越界了,或者 left 指向的值不是 target,说明没找到
if(left >= nums.size() || nums[left] != target) {
return {-1, -1};
}
return {left, right};
}
};
1.3 二分算法的时间复杂度

2. ⼆分查找
2.1 ⽜可乐和魔法封印
https://ac.nowcoder.com/acm/problem/235558
⼆分查找算法练手题,直接上⼿模版即可。
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int a[N];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
int T;
cin>>T;
while(T--)
{
int x,y;
cin>>x>>y;
int l=1;
int r=n;
while(l<r)
{
int mid=l+(r-l)/2;
if(a[mid]>=x)
{
r=mid;
}
else
{
l=mid+1;
}
}
//最小范围都大于输入元素的最大值
if(a[l]<x)
{
cout<<0<<endl;
continue;
}
int tl=l;
l=1,r=n;
while(l<r)
{
int mid=l+(r-l+1)/2;
if(a[mid]<=y)
{
l=mid;
}
else
{
r=mid-1;
}
}
//最大范围的数都小于输入元素的最小值
if(a[l]>y)
{
cout<<0<<endl;
continue;
}
cout<<l-tl+1<<endl;
}
}
2.2 A-B 数对
https://www.luogu.com.cn/problem/P1102

这道题的最优解其实是用哈希表 之前的博客写过这种解法 点击转跳
这里提供
二分的解法思路 希望可以拓展大家解题的思路和想法
题目性质:元素的顺序是不影响最终结果的!
把整个数组排序
通过
B=A-C即枚举A 然后利用二分查找(同时B肯定是小于A的)有多少个B!
【STL 的使⽤】
lower_bound:传入要查询区间的左右迭代器(注意是左闭右开的区间,如果是数组就是左右指针)以及要查询的值 (k),然后返回该数组中 (>= k) 的第一个位置;
upper_bound:传入要查询区间的左右迭代器(注意是左闭右开的区间,如果是数组就是左右指针)以及要查询的值 (k),然后返回该数组中 (> k) 的第一个位置;比如:(a = [10, 20, 20, 20, 30, 40]),设下标从 1 开始计数,在整个数组中查询 20:
lower_bound(a + 1, a + 1 + 6, 20),返回 (a + 2) 位置的指针;upper_bound(a + 1, a + 1 + 6, 20),返回 (a + 5) 位置的指针;- 然后两个指针相减,就是包含 20 这个数区间的长度。
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e5+10;
LL n, c;
LL a[N];
int main()
{
cin >> n >> c;
for(int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + 1 + n);
LL ret = 0;
for(int i=2;i<=n;i++)
{
LL b = a[i] - c;
//利用了b绝对不会大于a[i]的特性
ret += upper_bound(a + 1, a + i, b) - lower_bound(a + 1, a + i, b);
}
cout << ret << endl;
}
2.3 烦恼的高考志愿
https://www.luogu.com.cn/problem/P1678
先把学校的录取分数「排序」,然后针对每一个学生的成绩 b b b,在「录取分数」中二分出 ≥ b \ge b ≥b 的「第一个」位置 p o s pos pos(大于等于 归属于查找起始位置的模版),那么差值最小的结果要么在 p o s pos pos 位置,要么在 p o s − 1 pos - 1 pos−1 位置。取 a b s ( a [ p o s ] − b ) abs(a[pos] - b) abs(a[pos]−b) 与 a b s ( a [ p o s − 1 ] − b ) abs(a[pos - 1] - b) abs(a[pos−1]−b) 两者的「最小值」即可。
细节问题:
- 如果所有元素都大于 b b b 的时候, p o s − 1 pos - 1 pos−1 会在 0 下标的位置,有可能结果出错;
解决方法:1. 所有元素大于b的时候单独考虑就行
if(x<=a[1]) { ret+=abs(a[1]-x); continue; }
- 加个左右护法
a[0] = -1e7 + 10;这样哪怕是求min值 abs(a[0]-x)绝对大于abs(a[1]-x)!
- 如果所有元素都小于 b b b 的时候, p o s pos pos 会在 n n n 的位置,此时结果倒不会出错,(因为比如说
100 200 300你估分500min(500-300,500-200)最后肯定是500-300值小 最后结果不会有影响!)但是我们要想到这个细节问题,这道题不出错不代表下一道题不出错。
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
int n,m;
LL a[N];
int main()
{
cin>>m>>n;
for(int i=1;i<=m;i++)
{
cin>>a[i];
}
sort(a+1,a+1+m);
LL ret=0;
while(n--)
{
int x;
cin>>x;
int left=1;int right=m;
if(x<=a[1])
{
ret+=abs(a[1]-x);
continue;
}
while(left<right)
{
int mid=left+(right-left)/2;
if(a[mid]>=x) right=mid;
else left=mid+1;
}
ret+=min(abs(a[left]-x),abs(a[left-1]-x));
}
cout<<ret;
}
3. 二分答案
准确来说,应该叫做 二分答案 + 判断。
二分答案可以处理大部分 最大值最小 以及 最小值最大 的问题。如果 解空间 在从小到大的变化 过程中,判断答案的结果出现二段性,此时我们就可以二分这个解空间,通过判断,找出最优解。
刚接触的时候,可能觉得这个算法原理很抽象。没关系,3 道例题的详细解析后,你会发现这个二分答案的原理其实很容易理解,重点是如何去判断答案的可行性。
看不懂定义也没事 咱们直接从例题中讲解算法!
3.1 木材加工(二分答案模版题)
https://www.luogu.com.cn/problem/P2440
x表示:切割出来的小段长度
c表示:在x的基础下,最多能切出来多少段
k表示:最终要切割的段数
一. 先从暴力解法开始想(例子就是样例):
- 枚举所有的切割长度
x(我能从0枚举到456),然后求出在x的情况下,能够切出来的段数c- 我们用
a={232,124,456}然后直接通过循环c+=a[i]/x即可 如果c>=7则符合要求,我么只需要找出最大的那个x即可!
在暴力解的过程中 我们很容易能找到以下规律:
随着
x的增大,c一定是在减小的。结合这个性质,我们可以倒着枚举
x,当碰到第一个c>=7时,就是这道题目的答案!
二:解法二:
利用二分进行优化!(
从后往前枚举!)我们假设一个点
x=ret在这个点我们切出来的最大段数是大于等于k的(即此时ret点为最终结果)!如果枚举的长度大于ret那么此时我么能切出来的最大段数一定是小于k的!那么包括ret这个点往左的区域我们能切割出来的段数 一定是大于等于k的!
此时 我们可以说 这道题目是有二段性质的!
- 我们定义两个变量
left=0 right=maxlen然后定一个函数calc(x) 用来计算能切出来多少段!- 如果我们发现
calc(mid)>=k说明此时我们的mid落在了左边的区域:
所以此时left=mid。- 当
calc(mid)<k的时候 此时mid落在右边区域:
所以,此时right=mid-1
细节问题,此时求的
mid-1中点就用mid=(left+right+1)/2就行了,详情可以看1.1部分或者自己举个简单例子就行 因为会造成死循环
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
LL n,k;
LL a[N];
LL calc(LL x)
{
LL cnt=0;
for(int i=1;i<=n;i++)
{
cnt+=a[i]/x;
}
return cnt;
}
int main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
//注意 left必须从0开始 因为可能总长度达不到需要的段数(比如共100cm木头 要砍101份)那么此时的c>=k时 x只能等于0!!!
LL left=0,right=1e8;
while(left<right)
{
LL mid=(left+right+1)/2;
if(calc(mid)>=k) left=mid;
else right = mid-1;
}
cout<<left<<endl;
return 0;
}
二分答案算法小总结
之前看不懂二分答案的定义是啥 现在解决完3.1 这里的二分指的就是答案的二分!可以处理大部分 最大值最小 以及 最小值最大 的问题!结合上面的例题 其实就是在一堆偏小的结果中(比如说分7段可以切1cm 也可2cm 但是3cm最大)找到那个最大值的问题!
即最小中最大 最大中最小!我们可以切割答案 找到二段性即可!
3.2 砍树
https://www.luogu.com.cn/problem/P1873
练手题 如果3.1知识点掌握 那么这道题很简单!设伐木机的高度为 H,能得到的木材为 C。根据题意,我们可以发现如下性质,:
- 当 H 增大的时候,C 在减小;
- 当 H 减小的时候,C 在增大。
那么在整个「解空间」里面,设最终的结果是 ret,于是有:
当 H ≤ ret 时,C ≥ M。也就是「伐木机的高度」大于等于「最优高度」时,能得到的木材「小于等于」M;
当 H > ret 时,C < M。也就是「伐木机的高度」小于「最优高度」时,能得到的木材「大于」M。
在解空间中,根据 ret 的位置,可以将解集分成两部分,具有「二段性」,那么我们就可以「二分答案」。
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e6+10;
LL a[N];
int n,m;
LL sum(int mid)
{
LL sum=0;
for(int i=1;i<=n;i++)
{
if(mid<a[i])
{
sum+=(a[i]-mid);
}
}
return sum;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
LL l=0,r=1e9;
while(l<r)
{
LL mid=(l+r+1)/2;
if(sum(mid)>=m) l=mid;
else r=mid-1;
}
cout<<l;
}
3.3 跳石头(挺难的 难在找二分性~)
https://www.luogu.com.cn/problem/P2678
x:表示最短的跳跃距离(范围是[1,L])
c表示:在跳跃距离为x的情况下移走的岩石数
寻找二段性:
首先需要发现一个规律
- 当 (x) 增大的时候,(c) 也在增大;
- 当 (x) 减小的时候,(c) 也在减小。
那么在整个「解空间」里面,设最终的结果是 r e t ret ret,于是有:
- 当 x ≤ r e t x \le ret x≤ret 时, c ≤ M c \le M c≤M。也就是「每次跳的最短距离」小于等于「最优距离」时,移走的石头块数「小于等于」 M M M;
- 当 x > r e t x > ret x>ret 时, c > M c > M c>M。也就是「每次跳的最短距离」大于「最优距离」时,移走的石头块数「大于」 M M M。
在解空间中,根据 r e t ret ret 的位置,可以将解集分成两部分,具有「二段性」,那么我们就可以「二分答案」。
我们假设一个函数
calc(x)表示当跳跃距离为x的情况下,移走的岩石个数。
calc这个函数怎么实现呢?(这个其实也不简单)
- 定义前后两个指针 i , j i, j i,j 遍历整个数组,设 i ≤ j i \le j i≤j,每次 j j j 从 i i i 的位置开始向后移动;
- 当第一次发现 a [ j ] − a [ i ] ≥ x a[j] - a[i] \ge x a[j]−a[i]≥x 时,说明 [ i + 1 , j − 1 ] [i + 1, j - 1] [i+1,j−1] 之间的石头都可以移走;
- 然后将 i i i 更新到 j j j 的位置,继续重复上面两步。
cpp
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 5e4 + 10;
LL l, n, m;
LL a[N];
// 当最短跳跃距离为 x 时,移⾛的岩⽯数⽬
LL calc(LL x)
{
LL 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 ;//因为for循环有个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++;
LL left = 1, right = l;
while(left < right)
{
LL mid = (left + right + 1) / 2;
if(calc(mid) <= m) left = mid;
else right = mid - 1;
}
cout << left << endl;
return 0;
}




























