对二分算法的理解
二分算法是一种事半功倍的搜索算法,能运用二分的前提是数组要排好序。二分算法的整体思路如下:
数组搜索某数x的思路
已知某数组a:1、2、3、4、5、6、7、8、9、10、11、12、13:
- 首先定位L和R :L=0,首次定位在数组首位;R=len(a)-1,首次定位在数组尾部;
- 首先定位mid:mid=(L+R+1)/2;
- while(L<R) 执行循环,反复改变L、R、mid的值:如果x<a[mid],R=mid-1,否则L=mid+1。执行mid=(L+R+1)/2;
- 循环结束后,输出L或者R的值:如果与x不相等,则a数组中没有x;否则相等;
二分算法的难点
二分算法的难点在于如何对取到的边界值进行处理以及如何判断先抛弃左半部分还是先抛弃右半部分:当题目告诉你从右半部分着手解题时,先移动R(右半边界) ,反之亦然:
- 如果搜索a,令x<=a,a属于数组中最小的那一个:(先操作右部)
while(l<=r)
{
int mid=(l+r)>>1; //mid取中;
if(a[mid]>=x) r=mid; //对L、R进行移动的模板 ,这里取左边
else l=mid+1;//这里取右边
}
return a[l]; //或者L
- 如果搜索a,令x>=a,a属于最大的那一个:(先操作左部)
while(l<r)
{
int mid=(l+r+1)>>1; //取中,因为左移1的操作是去尾,所以“L+R+1”,让mid尽量兼顾中间靠右的数
if(a[mid]<=x) l=mid; //这里取右边
else r=mid-1; 这里取左边
}
return a[l];
接下来举个栗子:
例一 洛谷 砍树
题目描述
伐木工人米尔科需要砍倒M米长的木材。这是一个对米尔科来说很容易的工作,因为他有一个漂亮的新伐木机,可以像野火一样砍倒森林。不过,米尔科只被允许砍倒单行树木。
米尔科的伐木机工作过程如下:米尔科设置一个高度参数H(米),伐木机升起一个巨大的锯片到高度H,并锯掉所有的树比H高的部分(当然,树木不高于H米的部分保持不变)。米尔科就行到树木被锯下的部分。
例如,如果一行树的高度分别为20,15,10和17,米尔科把锯片升到15米的高度,切割后树木剩下的高度将是15,15,10和15,而米尔科将从第1棵树得到5米,从第4棵树得到2米,共得到7米木材。
米尔科非常关注生态保护,所以他不会砍掉过多的木材。这正是他为什么尽可能高地设定伐木机锯片的原因。帮助米尔科找到伐木机锯片的最大的整数高度H,使得他能得到木材至少为M米。换句话说,如果再升高1米,则他将得不到M米木材。
输入格式
第1行:2个整数N和M,N表示树木的数量(1<=N<=1000000),M表示需要的木材总长度(1<=M<=2000000000)
第2行:N个整数表示每棵树的高度,值均不超过1000000000。所有木材长度之和大于M,因此必有解。
输出格式
第1行:1个整数,表示砍树的最高高度。
输入
5 20
4 42 40 26 46
输出
36
题后反思
这道题让我深刻的明白了对待二分问题,一定要找好先对待左部还是右部,不然会WA。这个题的整体思路就是先找到最高的那棵树,最低的树是1,然后在最高R和最低L之间进行二分活动。又因为树是先从高的地方砍,所以要先对右部进行操作,先改变R,再改变L,并且mid=(L+R)>>1。
上代码:
代码提交
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e6+7;
ll a[maxn],R,L,mid;
int main()
{
ll M,N,mx=0;
cin>>M>>N;
for(ll i=0;i<M;i++){
cin>>a[i];
mx=max(mx,a[i]);
}
L=1;
R=mx;
while(L<=R){
mid=(L+R)>>1; //二分,缩短搜索的范围
ll sum=0;
for(ll i=0;i<=M;i++){ //对砍掉当前高度所得到的木材求和
if(a[i]>mid){
sum+=a[i]-mid;
}
}
if(sum<N){ //如果和大于要求,所求值位于右部
R=mid-1;
}
else{ //如果和小于要求,所求值位于左部
L=mid+1;
}
}
cout<<R;
return 0;
}
例二 跳石头
题目背景
一年一度的“跳石头”比赛又要开始了!
题目描述
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 NNN 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 MMM 块岩石(不能移走起点和终点的岩石)。
输入格式
第一行包含三个整数 L,N,ML,N,ML,N,M,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证 L≥1L \geq 1L≥1 且 N≥M≥0N \geq M \geq 0N≥M≥0。
接下来 NNN 行,每行一个整数,第 iii 行的整数 Di(0<Di<L)D_i( 0 < D_i < L)Di(0<Di<L), 表示第 iii 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。
输出格式
一个整数,即最短跳跃距离的最大值。
输入输出样例
输入
25 5 2
2
11
14
17
21
输出
4
说明/提示
输入输出样例 1 说明:将与起点距离为 222和 141414 的两个岩石移走后,最短的跳跃距离为 444(从与起点距离 171717 的岩石跳到距离 212121 的岩石,或者从距离 212121 的岩石跳到终点)。
另:对于 20%20%20%的数据,0≤M≤N≤100 ≤ M ≤ N ≤ 100≤M≤N≤10。
对于50%50%50%的数据,0≤M≤N≤1000 ≤ M ≤ N ≤ 1000≤M≤N≤100。
对于 100%100%100%的数据,0≤M≤N≤50,000,1≤L≤1,000,000,0000 ≤ M ≤ N ≤ 50,000,1 ≤ L ≤ 1,000,000,0000≤M≤N≤50,000,1≤L≤1,000,000,000。
解题思路
此题和上面的题刚好是相反的,上面的要找大于x的最小数,该题要找小于x的最大数,所以要搜索左部。具体模板在上方。
跳石头给出了起点1和终点de,答案就在起点和终点之间的某个值。二分除了模板,最要值得思考的就是对mid的判断。该题中,要判断中点mid与a数组中每个值的差,如果差小于mid,说明该石头可以被拿走。否则,就将当前石头作为被减数进行循环。将数组a中的数循环结束后,判断一下可以被拿走的石头数,如果小于要求x,则说明mid比最终结果小,抛弃左部,选择右部,即L=mid;否则选取左部,抛弃右部。最终输出L,就是最终结果。
代码如下:
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=5*1e5+7;
ll a[maxn],R,L,mid;
int main()
{
ll de,num1,num2;
cin>>de>>num1>>num2;
for(ll i=1;i<=num1;i++){
cin>>a[i]; //第i块岩石相对于起点的位置
}
L=1;
R=de; //de是终点
while(L<R){
ll now=0,num3=0;
mid=(L+R+1)>>1; //二分,缩短搜索的范围,所要求的答案位于1到de;
for(ll i=1;i<=num1;i++){ //搜索每跳一步的距离与mid的关系
if(a[i]-a[now]<mid) //该石头可以拿走
num3++;
else
now=i; //将该石头作为被减数
}
if(num3<=num2){ //如果去除的石头数目不够或者相等,说明所求值位于右部
L=mid;
}
else{ //如果去除的石头数目大于要求,所求值位于左部
R=mid-1;
}
}
cout<<L;
return 0;
}
以上就是我对二分算法的一点理解,希望各位大佬不吝赐教,积极指正鸭@><@