二分法是一种高效的查找方法,它通过将问题的搜索范围一分为二(两边具有明显的区别),迭代地缩小搜索范围,直到找到目标或确定目标不存在。
二分法适用于有序数据集合,并且每次迭代可以将搜索范围缩小一半。
二分法本质上也是枚举,但和暴力枚举不同,二分法利用数据结构的单调性减少了很多不必要的枚举,从而极大地提高了效率,一般可以将O(n)的枚举优化到O(logn)。
常见的二分类型有:
1)整数二分
2)浮点二分
3)二分答案(最常见)
1.研究并发现数据结构(或答案变量)的单调性。
2.确定最大区间1,,确保分界点一定在里面,具体有一些细节:若以r作为答案,那么答案区间在[1+1,r],若以1作为答案,那么答案区间在[1,r-1]。
3.确定check函数,一般为传入mid(区间中某个下标),返回mid所属区域或返回一个值,当check函数较简单时可以直接判断。
参数
4.计算中点mid=(1+r)/2,用check判断该移动l或r指针,具体移动哪个需要根据单调炒以及要求的答案来判断。
5.返回1或r,根据题意判断。
整数二分就是在一个已有的有序数组上,进行二分查找,一般为找出某个值的位置或者是找出分界点。
这个数组肯定是开的下的,其数组大小一般在1e6以内。区域划分如左图。
二分答案是二分法中最常见也最重要的题型,考察的比较灵活,需要选手从题目中发现某个单调的函数,然后对其进行二分。
常见的模型是:
二分框架(时间复杂度O(logm)+ check函数(时间复杂度O(n)
一般情况下,我们会将答案进行二分,然后在枚举出某个可能解后判断其是否可以更优或者是否合法,从而不断逼近最优解。
二分答案的题的特征:如果已知某个答案,很容易判断其是否合法或更优。某些贪心问题可能可以转化成二分答案问题。
123
思路:用前缀和枚举,复杂度高,只能过 40%。这题需要利用数学解来降低复杂度。找到数学规律,利用二分求出第 l,和 r 位的组数,再求出位数,有了组数和位数就能通过数学规律或前缀和求出 l 和 r 位的值。
样例输入
3
1 1
1 3
5 8
样例输出
1
4
8
#include<iostream>
using namespace std;
using ll = long long;
//const int N = 2e6+10;
//int pre[N];
int T;
ll getPre(ll i){
return i*(i+1)*(i+2)/6;
}
ll cula(ll x){
ll i = 0,l = 0,r = 2e6;
while(l<=r){
ll mid = (l+r)>>1;
if(mid*(mid+1)/2 > x)r = mid - 1,i = mid;
else l = mid + 1;
}
ll j = x - (i-1)*(i-1+1)/2;
return getPre(i-1) + j*(j+1)/2;
// return pre[i-1]+j*(j+1)/2;
}
int main( ){
// pre[1] =1;
// for(int i=1;i<=2000000;i++)pre[i] = pre[i-1] + i*(i+1)/2;
cin>>T;
while(T--){
ll l,r;cin>>l>>r;
cout<<cula(r)-cula(l-1)<<'\n';
}
return 0;
}
跳石头
- 思路:发现当“最短跳跃距离”越长时,需要移走的石头数量也越多。于是就产生了单调性,我们通过二分“最短跳跃距离”,在已知“最短跳跃距离”的情况下容易O(n)计算需要搬走的石头的数量,找到分界点即可(即在至多搬走M块石头的情况下的最远跳跃距离)。
#include<iostream>
using namespace std;
const int N = 1e5;
using ll = long long;
ll l,n,m;
int a[N];
ll check(ll mid){
int res = 0,last = 0;
for(int i=1;i<=n;i++){
if(a[i]-a[last]<mid){
res++;
continue;
}
last = i;
}
if(l-a[last]<mid)return m+1;
return res;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>l>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
ll left = 0,r = 1e9+9;
while(left+1!=r){
ll mid = (left+r)/2;
if(check(mid)<=m)left=mid;
else r=mid;
}
cout<<left<<'\n';
return 0;
}
肖恩的苹果林
这题和上一题特征明显,要求的是“最大的最近距离”,当通过二分枚举出一个最近距离时,我们可以判断其合法性(即贪心地判断是否能够放得不m棵树苗),如果合法说明这个距离也许可以再变大,如果不合法就说明这个最近距离应该变小。
在这里“最近距离mid”和“种树的数量check(m是负相关的关系。
#include<iostream>
using namespace std;
const int N = 1e5+10;
using ll = long long ;
int n,m,a[N];
ll check(ll mid){
int res=0;
for(int i=1,lst=0;i<=n;i++){
if(lst&&a[i]-a[lst]<mid)continue;
res++,lst=i;
}
return res;
}
int main( ){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
ll l = 0,r=1e9+10;
while(l+1!=r){
ll mid = (l+r)>>1;
if(check(mid)>=m)l=mid;
else r=mid;
}
cout<<l<<'\n';
return 0;
}
肖恩的乘法表
思路:因为 k 和元素个数呈现单调性,所以可以利用二分枚举答案元素,通过用数学规律来计算出每行<k 的数得到全部<k 的数来判断是否答案满足,从而利用二分夹逼得到最终答案,注意边界r 是满足条件的最小值。
通过排名去计算元素是不好算的,但是如果已知一个元素可以利用矩阵每一行的规律来O(n)地计算排名。
所以我们枚举元素大小,设rnk(x)表示矩阵中小于等于x的元素的个数,那么对于第i行<=x的数字的个数就是min(m,x/ i)。
必然有rnk(l)<k,rnk(r)>=k,从而将整个整数域划分部分,最后返回r即可。
#include<iostream>
using namespace std;
using ll = long long;
ll n,m,k;
ll check(ll mid){
int res=0;
for(int i=1;i<=n;i++){
res+=min(m,mid/i);
}
return res;
}
int main( ){
cin>>n>>m>>k;
ll l = 0,r = 1e12;
while(l+1!=r){
ll mid = (l+r)>>1;
if(check(mid)>=k)r=mid;
else l=mid;
}
cout<<r<<'\n';
return 0;
}