看了曦行夜落-浅谈二分的边界问题后深有感触,特此写下此篇博客以作梳理。
简介
二分是一种求取极值的算法,通常是已知所求答案的范围,然后根据一系列约束条件对其进行控制使得到的答案最优。二分答案需要满足单调性,举个例子来看,我们要完求完成任务的最小代价(范围为1到100),我们考虑中间值50:
如果以50的代价可以完成任务,那么,我们以51~100的代价显然也可以完成任务。
如果以50的代价不能完成任务,那么,我们以0~50的代价就都不能完成任务。
正是因为这种单调性,才使得二分可以在log2n的时间内完成查找工作。
进行原理
我们接着考虑上面的例子,我们可以发现:
如果以50的代价可以完成任务,那么,我们以51~100的代价显然也可以完成任务。则,我们需要在0~50中去寻找更优的满足条件的解。
如果以50的代价不能完成任务,那么,我们以0~50的代价就都不能完成任务。则,我们只能通过去牺牲数据,在51~100中去需找满足条件的解。
这就是二分的进行机制,由此,我们可以发现,二分中真正重要的地方是中间条件的取值。
对于二分中间值的讨论
二分的中间值是二分过程中最容易出锅的地方, 对于是应该取mid + 1还是mid - 1又或是mid常常让人混乱,这里我们对其进行一定的梳理。
二分中的主要思想有以下三种:
- l 和 r 所代表的”成本值”均可行(即当前讨论的区间为[ l, r]),且存在一个最优解ans。
- l 和 r 所代表的“成本值”均可行,最后答案是 l 和 r。
- l 和 r 所代表的“成本值”只有 l 可行而 r 不可行(即当前讨论的区间是[l, r ) ),最终的答案是 l。
我们对前两种算法进行模板归纳:
思想1:储存答案
for (;l<=r;) { int mid=(l+r)/2; if (check(mid)) { ans=mid; r=mid-1; } else l=mid+1; } printf("%d",ans);
结束条件是l > r 时停止循环,此时答案记录为ans。(依旧以上面举到的例子求大于条件的最小值)
对于每次循环中的mid,都把当前的区间分为了三部分[l , mid - 1] , mid, [ mid + 1, r],这三部分中,我们根据mid处的值将问题分为两类。
- 当mid满足条件的时候,我们就把mid当作是的当前的最优解,然后去考虑[l, mid - 1]中有没有可以刷新当前最优解的更优解。
- 当mid不满足条件的时候,我们就考虑[mid + 1, r]中有没有可以满足条件的解。
求大于的最小值则正好相反。
思想2:不储存答案
while (l<r) { int mid=(l+r)/2; if (check(mid)) r=mid else l=mid+1; } printf("%d",l);
结束条件是 (l == r),此时l 和 r 相同且都是最优解。(依旧以上面的例子求大于条件的最小值)
对于每次循环中的mid,都把当前的区间分为了两部分[l, mid], [mid + 1, r],这两部分中我们根据mid的值将问题分为两类。
- 当mid满足条件的时候,我们把mid看作是一个可行解,根据二分的单调性我们可以知道[mid + 1, r]一定都满足条件且没有mid更优,所以我们可以得到更优的解一定是存在于[l, mid]中的。为什么要包括mid呢?我们可以这样来看,我们假设当mid就是满足条件的最优解而且这个区间中数的个数不只一个,如果我们只考虑[l, mid - 1]的话,就会因为漏解而使得最终答案错误。
- 当mid不满足条件的时候,根据单调性[l, mid]一定都不能满足条件,所以我们只需要在[mid + 1, r]中寻找满足条件的解即可。
反过来,如果我们求的是小于条件的最大值时,对于每次循环中的mid, 都把当前区间分为了两部分[l, mid - 1],[mid, r],两部分。
二分查找
二分查找就是最经典的二分答案,我们对于一个有序序列,记录查找值为key的数, 而对于一个为key的数,我们既可以把他看作是满足大于条件的最小值,也可以看作是满足小于条件的最大值。