一.二分算法
基本概念
- 二分法的核心思想——折半,每次查找都可以去除一半的数据量,直到最后将所有不符合条件 的结果都去除,只剩下一个符合条件的结果
- 这种比大小折半的算法的前提,就是查找的整数序列必须有序,所以我们才能与序列其中一个数比大 小的结果,来推断序列中其他数或大或小或不可知。
- 时间复杂度为O(nlogn),查找效率较高
二.二分查找
- 在升序排列的数列中查找数字k(数列中各个数字均不相同)
int l=1,r=n,Ans;//l,r是查找数列的下标区间的左右端点,Ans就是要找的答案
while(l<=r){
int mid=(l+r)/2;
if(a[mid]==k)//找到的数字k,记录答案并退出{
Ans=mid;
break;
}
else if(a[mid]<k) l=mid+1;//找到的数字比k小,说明答案在右半部分,收缩区间左端点
else if(a[mid]>k) r=mid-1;//找到的数字比k大,说明答案在左半部分,收缩区间右端点
}
- 单调不减的数列中查找第一个大于等于k的数字的位置
int l=1,r=n,Ans;//Ans就是要找的答案
while(l<=r){
int mid=(l+r)/2;
if(a[mid]==k)//找到的数字等于k,说明答案可能是当前位置或者更左边的位置,但一定不是右
半区间
{
r=mid-1;
Ans=mid;
}
else if(a[mid]<k) l=mid+1;//找到的数字比k小,说明答案在右半部分,收缩区间左端点
else if(a[mid]>k) r=mid-1;//找到的数字比k大,说明答案在左半部分,收缩区间右端点
}
简化版:
int l=1,r=n,Ans;//Ans就是要找的答案
while(l<=r){
int mid=(l+r)/2;
if(a[mid]<k) l=mid+1;
else if(a[mid]>=k) r=mid-1,Ans=mid;
}
- 单调不减序列中查找最后大于等于k的数字的位置
int l=1,r=n,Ans;//Ans就是要找的答案
while(l<=r){
int mid=(l+r)/2;
if(a[mid]>k) r=mid-1;
else if(a[mid]<=k) l=mid+1,Ans=mid;
}
三.二分答案
当我们考虑一个比较复杂的问题时,如果问题中预估的答案存在一个区间,我们同样可以对这个答案区间做二分,找到最优解。二分答案的技巧常用于一些求最大或最小符合要求的答案的题目
解题思路:
- 二分法找到一个可能答案
- 用这个可能答案代入题干,去验证其是否符合要求
- 根据第2步的结果,收缩答案区间,并记录答案。
例题:
1.木材加工
题目:
解析:
bool check(int mid){
int cnt=0;
for(int i=1;i<=n;++i) cnt+=L[i]/mid;
if(cnt>=k) return 1;
else return 0;
}//检验mid是否合法
int work(){
int l=1,r=1e8,Ans;
while(l<=r){
int mid=(l+r)/2;
if(check(mid)) Ans=mid,l=mid+1;
else r=mid-1;
}
return Ans;
}
2.跳石头
题目:
解析:
bool check(int mid){
int cnt=0;
int i=0,now=0;
while(i<n+1){
++i;
if(a[i]-a[now]<mid) ++cnt;//距离太小,删除第i个点
else now=i;
}//i表示点的编号,0代表起点,n+1代表终点,now表示当前点的编号
if(cnt<=m) return 1;
else return 0;
}
int work(){
int l=0,r=n,Ans;
while(l<=r){
int mid=(l+r)/2;
if(check(mid)) Ans=mid,l=mid+1;
else r=mid-1;
}
return Ans;
}
总结
- 二分答案问题,往往涉及到最大值最小和最小值最大两类要求,同时也普遍存在着正面很难解决,反 过来却比较好做的特点。
- 思考答案是否具备单调性和检验答案的方法
四.例题
1.二分查找
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int a[N];
bool find(int a[],int x,int n){
int l=0,r=n-1;
while (l<=r){
int mid=(l+r)/2;
if (a[mid]==x) return true;
if (a[mid]>x) r=mid-1;
else l=mid+1;
}return false;
}
int main(){
int n;
cin>>n;
for (int i=0 ;i<n ;i++) cin>>a[i];
int m;
cin>>m;
for (int i=0 ;i<m ;i++){
int x;
cin>>x;
if (find(a,x,n)) cout<<"YES\n";
else cout<<"NO\n";
}
}
解题思路:使用二分查找模板即可
2.A-B数对
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10;
int main(){
int n,c;
cin>>n>>c;
ll a[N],ans=0;
for (int i=0 ;i<n ;i++) cin>>a[i];
sort(a,a+n);
for (int i=0 ;i<n ;i++){
ans+=( upper_bound(a,a+n,a[i]+c) - lower_bound(a,a+n,a[i]+c));
}
cout<<ans;
}
解题思路:通过stl提供的二分查找(upper_bound与lower_bound)确定与当前A所对应B的个数
3.分巧克力
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10;
int n,k;
int h[N],w[N];
bool check(int x){
int cnt=0;
for (int i=0 ;i<n ;i++){
cnt += (h[i]/x) * (w[i]/x);
}
if(cnt>=k) return true;
else return false;
}
int main(){
cin>>n>>k;
for (int i=0 ;i<n ;i++) {
cin>>h[i]>>w[i];
}
int l=0,r=1e5;
int ans;
while(l<=r){
int mid = (l+r)/2;
if (check(mid)) {
ans = mid;
l = mid+1;
}else r = mid-1;
}
cout<<ans;
}
解题思路:通过check函数来检查数据mid是否符合要求,再通过二分答案法找到所求答案
4.卡牌
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10;
ll n; ll m;
ll a[N],b[N];
bool check(int x){
ll cnt=0;
for (ll i=0 ;i<n ;i++){
if (x-a[i]>0){
if (b[i]<x-a[i]) return false;
cnt+=(x-a[i]);
}
}
if (cnt<=m) return true;
else return false;
}
int main(){
cin>>n>>m;
for (ll i=0 ;i<n ;i++) cin>>a[i];
for (ll i=0 ;i<n ;i++) cin>>b[i];
ll l,r,ans;
r = 2*n;
l = 0;
while(l<=r ){
ll mid = (l+r)/2;
if (check(mid)) {
ans=mid;
l = mid+1;
}else r = mid-1;
}
cout<<ans;
}
解题思路:大致同上一题相同
5.书的复制
#include<bits/stdc++.h>
using namespace std;
long long n,m;
long long a[505];
int x[505],y[505]; //记录输出
bool check(int s) { //检查
int num=1,t=0;
for(int i=n;i>=1;i--) { //倒序
if(t+a[i]>s) t=0,num++; //换下一人
t+=a[i];
}
return num<=m; //人数是否足够
}
int find(int low,int high) { //二分
int mid;
while(low+1<high){
mid=low+(high-low)/2;
if(check(mid))
high=mid;
else
low=mid;
}
return high; //注意返回high
}
int main() {
long long low=0,high=0;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
high+=a[i]; //上界为所有书的和
low=max(low,a[i]); //下界为最厚书的页数
}
int s=find(low,high); //获取最优值
int t=0,num=1;
for(int i=1;i<=m;i++) //初始化输出数组
x[i]=y[i]=0;
y[1]=n; //第一人结束点为n
for(int i=n;i>=1;i--) { //倒序
if(t+a[i]>s) {
t=0;
x[num]=i+1; //第num人开始编号定为i+1
y[++num]=i; //第num+1人结束编号为i
}
t+=a[i];
}
x[num]=1; //最后一人起始点为1
for(int i=m;i>=1;i--) //倒序
cout<<x[i]<<" "<<y[i]<<endl;
return 0;
}
解题思路:通过二分答案法找到所需最短的复制时间,然后根据该时间来确定每个人所复制的书
6.青蛙过河
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n, x;
long long arr[N], sum[N];
bool check(int y) {
for(int i=y;i<=n-1;i++) if(sum[i] - sum[i - y] < 2 * x) return false;
return true;
}
int main() {
cin >> n >> x;
for(int i=1; i<=n - 1 ;i++) {
cin >> arr[i];
sum[i] = sum[i - 1] + arr[i];
}
int l = 1, r = n;
while(l < r) {
int mid = (l + r) / 2;
if(check(mid)) r = mid;
else l = mid + 1;
}
cout << r << endl;
return 0;
}
- 跳跃能力y符合要求 等价于 所有长度为 y 的区间内的石头高度和 >=2x
- 可以将这2x次的往返看成2x只青蛙从一端调到另一端
- 第一次跳跃这2x只青蛙分布在[ 1 ,y ]间
- 位于1上的青蛙在第二次跳跃中分布为[ 2 ,y+1 ],只要这段石头高度和>=2x,则符合要求
- 依次往后推,直到[ n-y ,n-1 ]就可以一次跳上岸了