二分查找模板
使用二分查找的前置条件
- 题目具有单调性,二分查找边界前后具有单调性
- 命题可以归纳为:求满足条件的最小值和求满足条件的最大值
- 最后得到一个答案
- 由第二个前置条件可以引申出两个模板:
找左边界的模板就是求满足条件的最小值。
有一个数组q[]={1,2,3,3,4,5}
,我想要找到第一个3的下标,而不是右边那个3
这就是求左边界.而找右边界就是找最右边的3的下标.就是求右边界.
模板代码:
bool check()
{
if(..)return true;
else return false;
}
int l=..,r=..;//初始化l,r,保证答案在区间[l,r]之间
//求左边界
//check()函数一定是存在于右边区域 比如说q[mid]>=x;x是要找的值,就可以说q[mid]存在于右边区域
while(l<r)
{
int mid=l+r>>1;//等价于(l+r)/2
if(check())r=mid;
else l=mid+1;
}
cout<<l;//左边界
//求右边界,check()函数一定存在于左边区域,比如说q[mid]<=x;
while(l<r)
{
int mid=l+r+1>>1;//等价于(l+r+1)/2
if(check())l=mid;
else r=mid-1;
}
cout<<r;//右边界
找左边界check()函数返回值图例:
1. 写题时的check()记忆方法:
先找题目中所求的内容时左边界还是右边界
-
如果求的是左边界,那么check()为真时,一定是r=mid绑定,搜索答案的区间从右边的区间缩小到左边,最后缩小到左边界,即答案
-
如果求的是右边界,那么check()为真时,一定是l=mid绑定,搜索答案的区间从左边的区间缩小到右边,最后缩小到右边界,即答案
2. 左边界和右边界中 mid取值的差异
2.1 左边界模板mid=l+r>>1
,右边界模板mid=l+r+1>>1
如果是l=mid就要int mid =l+r+1>>1,否则会陷入死循环
向下取整如果l=mid,就会死循环,向上取整同理用r=mid就会死循环
2.2 mid的记忆方法:男左女右:
男一般是1,男左代表着求左边界的时候,所以求左边界mid不需要+1
女一般是0,女右代表着求右边界的时候,所以求右边界mid需要+1
2.3 注意check函数内部if
语句的符号一定是<=
或者>=
,如以下代码的第四行:
这样可以在缩小搜寻范围的同时涵盖答案
bool check(int mid)
{
...
if(ans>=c)return true;//如果ans大于c就说明间距太小放很多牛,所以间距要扩大
else return false;
}
3. 如何更新搜寻答案区间的范围
比如说一个找右边界的模板:
if (q[mid]<=x)r=mid;
else l=mid+1;
q[mid]<=x
只要有等号,就说明有可能答案有可能在这个范围内,因为我们的目标是查找q[mid]==x
,此时更新搜寻区域范围的时候
如果更新为r=mid-1
,会导致丢失答案,因此更新范围不能跳过mid这个值,答案区间更新为[l,mid]
else
部分也是同理:
q[mid]<x
表示答案一定不在这个范围内,因此更新搜寻区域范围可以跳过mid这个值,答案区间更新为[mid+1,r]
例题1:数的范围 Acwing模板题
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。
如果数组中不存在该元素,则返回 -1 -1。
输入格式
第一行包含整数 n 和 q ,表示数组长度和询问个数。
第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。
接下来 q 行,每行包含一个整数 k ,表示一个询问元素。
输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1。
数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
代码部分
#include<bits/stdc++.h>
using namespace std;
int n,q,x;
const int N=1e6+10;
int a[N];
int main()
{
cin>>n>>q;
for (int i=0;i<n;i++)cin>>a[i];
while(q--)
{
int l=0,r=n-1;
cin>>x;
//找左边界
while(l<r)
{
int mid=l+r>>1;
if (a[mid]>=x)r=mid;
else l=mid+1;
}
if (a[l]!=x)cout<<"-1"<<' ';
else cout<<l<<' ';
//找右边界
l=0,r=n-1;
while(l<r)
{
int mid=l+r+1>>1;
if (a[mid]<=x)l=mid;
else r=mid-1;
}
if (a[l]!=x)cout<<"-1"<<' ';//找不到的情况下输出-1
else cout<<l<<' ';
puts("");
}
return 0;
}
注意点
判断条件很重要!!
关于判断条件能不能改:
在这个特定的二分查找算法中,如果将 if(a[mid] <= k) 改为 if(a[mid] >= k),并相应地调整后续的语句,
这会改变算法的行为。这样的修改将导致你在找右边界时,当 a[mid] 大于等于 k 时,
将 l 移动到 mid 的位置,否则将 r 移动到 mid 的位置。这实际上是在找左边界,而不是右边界了。
因此,这样的修改将导致算法不再有效。在二分查找算法中,需要根据问题的特定要求来确定条件的方向,通常情况下,
我们是根据需要找的元素是在有序序列中的左侧还是右侧来确定条件的方向。在你的代码中,
因为你是要找右边界,所以条件 a[mid] <= k 是正确的。如果你想要找左边界,你需要修改为 a[mid] >= k,并相应地调整后续的逻辑。
例题2:木材加工 洛谷p2440
题目描述
木材厂有 n n n 根原木,现在想把这些木头切割成 k k k 段长度均为 l l l 的小段木头(木头有可能有剩余)。
当然,我们希望得到的小段木头越长越好,请求出 l l l 的最大值。
木头长度的单位是 cm \text{cm} cm,原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。
例如有两根原木长度分别为 11 11 11 和 21 21 21,要求切割成等长的 6 6 6 段,很明显能切割出来的小段木头长度最长为 5 5 5。
输入格式
第一行是两个正整数 n , k n,k n,k,分别表示原木的数量,需要得到的小段的数量。
接下来 n n n 行,每行一个正整数 L i L_i Li,表示一根原木的长度。
输出格式
仅一行,即 l l l 的最大值。
如果连
1cm
\text{1cm}
1cm 长的小段都切不出来,输出 0
。
样例 #1
样例输入 #1
3 7
232
124
456
样例输出 #1
114
提示
数据规模与约定
对于
100
%
100\%
100% 的数据,有
1
≤
n
≤
1
0
5
1\le n\le 10^5
1≤n≤105,
1
≤
k
≤
1
0
8
1\le k\le 10^8
1≤k≤108,
1
≤
L
i
≤
1
0
8
(
i
∈
[
1
,
n
]
)
1\le L_i\le 10^8(i\in[1,n])
1≤Li≤108(i∈[1,n])。
-
注意:找右边界的时候,
if(check())
为真,后面一定执行是l=mid
,代表着把区间搜索范围转移到右半区else r=mid-1
代表着把区间搜索范围转移到左半区. -
通过题目我们需要构造一个分界线,使得左边满足
check()
而右边不满足check()
-
check()
函数内部逻辑分析题目中已知我们把木头切成了
k
段,每根木头的长度是l
我们可以每次二分
l
的值,然后定义一个变量ans
,表示每次切下l长度的木头段数.每根原木切下
l
一次就ans++
目前分析下来check()函数内部逻辑是:
if
的括号中ans<=k
还是应该ans>=k
if(ans<=k)//... else//...
-
判断
if(ans<=k)
为真时,执行l=mid
是否正确**(找右边界的模板)**.
此时表示切的木头长度
l
太大,切的木头段数
ans
比所要求的要小,应该减少l
的长度,也就是把l的区间查找范围挪到左半区**.即:
if (ans<=k)return r=mid; else return l=mid+1;
不满足找右边界模板的规范
如果改为
if(ans>=k)
说明此次二分取的木头长度l
太小,导致切的木头段数太多,所以应该增加l
的值,把l的区间查找范围挪到右半区,因此此处判断条件为真的时候,后面应该跟
r=mid
,即:
if (ans>=k)return l=mid; else return r=mid-1;
满足找右边界模板的规范.即:
找右边界的时候,
if(check())
为真,后面一定执行是l=mid;
-
代表着满足条件的情况下,把区间搜索范围转移到右半区
-
else r=mid-1
代表着把区间搜索范围转移到左半区.
因此,
if(ans>=k)
应该修改为if(ans<=k)
,代码部分:
#include<bits/stdc++.h> using namespace std; long long n,k; const int N=1e5+10; long long q[N]; bool check(long long mid) { long long ans=0;//记录每次能切多少次 for(int i=0;i<n;i++) { ans+=q[i]/mid; } if (ans>=k)return true; else return false; } int main() { scanf("%d %d",&n,&k); for (int i=0;i<n;i++) { scanf("%d",&q[i]); } long long l=0,r=1e8; while(l<r) { long long mid=l+r+1>>1; if (check(mid)) l=mid; else r=mid-1; } cout<<l; return 0; }
-
例题3:进击的奶牛 洛谷p1824
进击的奶牛
题目描述
Farmer John 建造了一个有 N N N( 2 ≤ N ≤ 1 0 5 2 \leq N \leq 10 ^ 5 2≤N≤105) 个隔间的牛棚,这些隔间分布在一条直线上,坐标是 x 1 , x 2 , ⋯ , x N x _ 1, x _ 2, \cdots, x _ N x1,x2,⋯,xN( 0 ≤ x i ≤ 1 0 9 0 \leq x _ i \leq 10 ^ 9 0≤xi≤109)。
他的 C C C( 2 ≤ C ≤ N 2 \leq C \leq N 2≤C≤N)头牛不满于隔间的位置分布,它们为牛棚里其他的牛的存在而愤怒。为了防止牛之间的互相打斗,Farmer John 想把这些牛安置在指定的隔间,所有牛中相邻两头的最近距离越大越好。那么,这个最大的最近距离是多少呢?
输入格式
第 1 1 1 行:两个用空格隔开的数字 N N N 和 C C C。
第 2 ∼ N + 1 2 \sim N+1 2∼N+1 行:每行一个整数,表示每个隔间的坐标。
输出格式
输出只有一行,即相邻两头牛最大的最近距离。
样例 #1
样例输入 #1
5 3 1 2 8 4 9
样例输出 #1
3
- 思路:先看所求的内容,满足在能放下c头牛的条件求相邻牛相距的最大值,也就是满足条件的最大值
- 判断函数的单调性;
- 能够定下一个分界线ans(当前两头牛相邻距离的放置数),只要当前隔间和前一头牛的隔间相邻距离大于要求相隔的距离,则放下一头牛,视为一次合法的放置,ans++,若放置数ans大于牛的总数,则不满足条件,反之,则满足条件.因此,单调性证毕.
- 因此,可以用二分找右边界的方法来求解
代码部分
#include <bits/stdc++.h>
using namespace std;
int n,c;
const int N=1e5+10;
int q[N];
bool check(int mid)
{
int ans=0,cowIndex=-1e9;
//把cowIndex设成负无穷因为第一个端点一定要放牛
//ans是放了多少牛,第一头牛已经放好。
//cowIndex就是每次放牛的位置 (牛棚的距离)
for(int i=0;i<n;i++)
{
if (q[i]-cowIndex>=mid)//如果举例大于mid(mid是安全距离),就可以放牛
{
cowIndex=q[i];//如果是第一次进就是在1的位置放了牛
ans++;
}
}
if(ans>=c)return true;//当ans>=c说明此时能放下c头牛,和题目中要满足的条件一致(如果ans大于c就说明间距太小放很多牛,所以间距要扩大 )如果才成ans<=c,那么就说明在当前情况不能容纳下c头牛,check()条件就不一致
else return false;
}
int main()
{
int longest;
scanf("%d %d",&n,&c);
for(int i=0;i<n;i++)
{
scanf("%d",&q[i]);
longest=max(longest,q[i]);
}
sort(q,q+n);
int l=0,r=longest;
while(l<r)
{
int mid=l+r+1>>1;//找右边界
if (check(mid))l=mid;
else r=mid-1;
}
cout<<l;
return 0;
}
例题4:[COCI 2011/2012 #5] EKO / 砍树 洛谷p1873
[COCI 2011/2012 #5] EKO / 砍树
题目描述
伐木工人 Mirko 需要砍 M M M 米长的木材。对 Mirko 来说这是很简单的工作,因为他有一个漂亮的新伐木机,可以如野火一般砍伐森林。不过,Mirko 只被允许砍伐一排树。
Mirko 的伐木机工作流程如下:Mirko 设置一个高度参数 H H H(米),伐木机升起一个巨大的锯片到高度 H H H,并锯掉所有树比 H H H 高的部分(当然,树木不高于 H H H 米的部分保持不变)。Mirko 就得到树木被锯下的部分。例如,如果一排树的高度分别为 20 , 15 , 10 20,15,10 20,15,10 和 17 17 17,Mirko 把锯片升到 15 15 15 米的高度,切割后树木剩下的高度将是 15 , 15 , 10 15,15,10 15,15,10 和 15 15 15,而 Mirko 将从第 1 1 1 棵树得到 5 5 5 米,从第 4 4 4 棵树得到 2 2 2 米,共得到 7 7 7 米木材。
Mirko 非常关注生态保护,所以他不会砍掉过多的木材。这也是他尽可能高地设定伐木机锯片的原因。请帮助 Mirko 找到伐木机锯片的最大的整数高度 H H H,使得他能得到的木材至少为 M M M 米。换句话说,如果再升高 1 1 1 米,他将得不到 M M M 米木材。
输入格式
第 1 1 1 行 2 2 2 个整数 N N N 和 M M M, N N N 表示树木的数量, M M M 表示需要的木材总长度。
第 2 2 2 行 N N N 个整数表示每棵树的高度。
输出格式
1 1 1 个整数,表示锯片的最高高度。
样例 #1
样例输入 #1
4 7 20 15 10 17
样例输出 #1
15
样例 #2
样例输入 #2
5 20 4 42 40 26 46
样例输出 #2
36
提示
对于 100 % 100\% 100% 的测试数据, 1 ≤ N ≤ 1 0 6 1\le N\le10^6 1≤N≤106, 1 ≤ M ≤ 2 × 1 0 9 1\le M\le2\times10^9 1≤M≤2×109,树的高度 ≤ 4 × 1 0 5 \le 4\times 10^5 ≤4×105,所有树的高度总和 > M >M >M。
-
思路:
-
所求的答案只有一个,满足获得足够木材的条件,求锯片的最高高度,也就是求满足条件的最大值
-
判断函数的单调性:锯片的高度越高,获得的木材数量越少,锯片高度越小,获得的木材越多,因此check()满足的条件就是获得足够木材,可以定下一个分界线sum(收集砍下的木材总和),当收集的木材大于所需的木材就满足条件,反之不满足条件,单调性证毕.
-
因此,可以使用二分找右边界的方法
-
如果本题改为,锯片的高度越高,获得的木材越多,那就是求满足条件的最小值了,也就是刚收集到足够木材时,锯片的高度
代码部分:
#include<bits/stdc++.h>
using namespace std;
long long n,m;
const int N=1e6+10;
long long q[N];
bool check(long long mid)
{
long long sum=0;
for (int i=0;i<n;i++)
{
if (q[i]>mid)
{
sum+=q[i]-mid; //sum是在当前锯片高度下收集的木材总和
}
}
if (sum>=m)return true;//当sum大于的时候说明收集的木材足够,和题目中给的条件重合。(锯片太短要变长,mid需要往右靠)
else return false;
}
int main()
{
long long longest=0;
scanf("%lld %lld",&n,&m);
for (int i=0;i<n;i++)
{
scanf("%lld",&q[i]);
longest=max(longest,q[i]);
}
// for (int i=0;i<n;i++)cout<<q[i]<<endl;
long long l=0,r=longest;
while(l<r)
{
long long mid=l+r+1>>1;
if (check(mid))l=mid;
else r=mid-1;
}
printf("%lld",l);
return 0;
}