目录
简介:
二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。实质是验证位置是否符合要求,将数组分为符合要求的区间与不符的区间,找到临界位置。
但值得注意的是:二分查找主要针对的是有序数组。
模板:
对于一个长度为N,区间为【0,N-1】的数组,可以分别设置左,右为边界位置的前后一个点,在按照模板将区间划分成符合要求的两个区块,最后按 check函数的设置决定取哪个点进行算法。这个方法重要的一步是检查最后返回的left或right是否越界,依次来判定返回值。
详情的模板解释可以去看B站五点七边的视频,这里主要进行刷题的讲解。
一:二分查找最先出现的数字
对于有序数组的查找,最先想到二分查找,IsBule的条件可以写为:要验证的位置是否小于所要查找的值,是则将该验证位置+其左边的数组全部置为小于查找值的区间。最后只要验证right位置的值是否为要查找的值,是则返回right,否则返回-1;
code:
#include<iostream>
using namespace std;
int SearchNum(int*a,int val,int size)
{
int left = -1, right = size;
while (left + 1 != right)
{
int mid = (left + right) / 2;
if (a[mid] < val)
{
left = mid;
}
else
{
right = mid;
}
}
if (right!=size&&a[right] == val)
return right+1;//返回下标
else
return -1;
}
int main()
{
int n, m;
cin >> n >> m;
int* a = new int[n];
for (int i = 0; i < n; i++)
{
cin >> a[i];
}
while (m--)
{
int x;
cin >> x;
printf("%d ", SearchNum(a,x,n));
}
return 0;
}
二:A-B数对---查找某个值的变形题目
对于A-B=C的数对,实际上可以看成B+C=A的数对个数,这里先将数组排列有序,for循环遍历赋值给B,实际则查找数组中B+C数值点的个数,二分查找即可。可以先查找A第一次出现的位置,在查找A出现的最后一次的位置,两者相减+1则得到A的个数,注意验证A不存在的情况。
code:
#include<iostream>
#include<algorithm>
using namespace std;
int first_A(int* a, int A, int size)
{
int left = -1, right = size;
while (left + 1 != right)
{
int mid = (left + right) / 2;
if (a[mid] < A)
{
left = mid;
}
else
{
right = mid;
}
}
if (right!=size&&a[right] == A)
return right;
else
return 0;
}
int last_A(int* a, int A, int size)
{
int left = -1, right = size;
while (left + 1 != right)
{
int mid = (left + right) / 2;
if (a[mid] <= A)
{
left = mid;
}
else
{
right = mid;
}
}
if (left!=-1&&a[left] == A)
return left;
else
return 0;
}
long long A_B_num(int*a, int val, int size)
{
long long sum = 0;
sort(a, a + size);
for (int B = 0; B < size; B++)
{
int A = a[B] + val;
if (first_A(a, A, size) && last_A(a, A, size))
{
sum += last_A(a, A, size) - first_A(a, A, size) + 1;
}
}
return sum;
}
int main()
{
int n, c;
cin >> n >> c;
int* a = new int[n];
for (int i = 0; i < n; i++)
cin >> a[i];
cout << A_B_num(a,c,n) << endl;
return 0;
}
三 :砍树问题---面向结果验证答案是否符合要求
先记录最高树木的高度为highest,要查找的最大高度一定在【1,highest】之间,所以用二分查找验证mid高度是否符合要求。这里要查找的数组实际是答案数组,只要找出符合题目要求的区间取最大值即可。
code:
#include<iostream>
#include<algorithm>
using namespace std;
//size--树的个数,need--需要的木头高度,pos--当前要砍的高度位置
bool is_MoreNeed(int* a, int size, int need,int pos)
{
int sum = 0;
for (int i=0;i<size;i++)
{
sum += max(0, a[i] - pos);
if (sum >= need)
{
return true;
}
}
return false;
}
int Height_Max(int* a, int size, int need,int highest)
{
int left = 0, right = highest;
while (left + 1 != right)
{
int mid = (left + right) / 2;
if (is_MoreNeed(a,size,need,mid))
{
//寻找临界高度
left = mid;
}
else
{
right = mid;
}
}
return left;
}
int main()
{
int n, m;
cin >> n >> m;
int* a = new int[n];
int highest = 0;
for (int i = 0; i < n; i++)
{
cin >> a[i];
highest = max(highest, a[i]);//记录最高树的高度
}
cout<<Height_Max(a,n,m,highest)<<endl;
return 0;
}
四:木材加工---面向结果验证答案
求加工出的木材最大长度一定在【0,longest】长度之间,只需验证每次二分的mid是否符合题目要求即可(验证加工出的木材是否大于等于k)。
code:
#include<iostream>
#include<algorithm>
using namespace std;
int longest = 0;
//pos--目标切割长度
bool More_Num(int *a,int size,int need,int pos)
{
int sum = 0;
for (int i = 0; i < size; i++)
{
sum += a[i] / pos;
if (sum >= need)
{
return true;
}
}
return false;
}
//need--需要的等长的木材个数,size--总木材个数
int LongestNum(int*a,int size,int need,int longest)
{
//答案在【left,right】区间内,找到临界位置即可
int left = 0, right = longest;
while (left + 1 != right)
{
int mid = (left + right) / 2;
if (More_Num(a,size,need,mid))
{
left = mid;
}
else
{
right = mid;
}
}
return left;
}
int main()
{
int n, k;
cin >> n >> k;
int* a = new int[n];
for (int i = 0; i < n; i++)
{
//记录每个木头的长度
cin >> a[i];
longest = max(longest, a[i]);
}
cout << LongestNum(a,n,k,longest) << endl;
return 0;
}
五:一元三次方程---二分缩小查找区间(精确浮点数)
依次验证【-100,-99】,【-99,-98】...【99,100】等区间有没有所求根---利用提示写判定条件,当验证出在某个小区间内时,令left,right分别为左右边界,利用二分查找验证根是否在mid,right之间,是则令left=mid,否则令right=mid;精度尽量小一点便于找精确值。
code:
#include<cstdio>
double a,b,c,d;
double fc(double x)
{
return a*x*x*x+b*x*x+c*x+d;
}
int main()
{
double l,r,m,x1,x2;
int s=0,i;
scanf("%lf%lf%lf%lf",&a,&b,&c,&d); //输入
for (i=-100;i<100;i++)
{
l=i;
r=i+1;
x1=fc(l);
x2=fc(r);
if(!x1)
{
printf("%.2lf ",l);
s++;
} //判断左端点,是零点直接输出。
//不能判断右端点,会重复。
if(x1*x2<0) //区间内有根。
{
while(r-l>=0.000001) //二分控制精度。
{
m=(l+r)/2; //middle
if(fc(m)*fc(r)<=0)
l=m;
else
r=m; //计算中点处函数值缩小区间。
}
printf("%.2lf ",r);
//输出右端点。
s++;
}
if (s==3)
break;
//找到三个就退出大概会省一点时间
}
return 0;
}
六: 跳石头---最小值最大问题
因为答案肯定还是在【0,L】之间,所以还是面向答案二分验证答案是否符合要求。
这里因为是求最小值的尽可能大值,所以left区间是符合要求的,只需找到最大值left即可。
check方法是保证石头间隔小于等于mid验证值,不然则搬走一块石头,cnt++,最后要搬的石头数小于等于要求数即说明验证值符合left区间,以此划分区间即可。
code:
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
//size--石头个数,pos--要验证的答案
bool check(int*a,int size,int pos,int need)
{
//目的是验证该最大距离是否符合要求,即要使最小距离大于等于目标pos,计算搬走的石头是否小于等于要求的次数M。
//now--现在站立的位置,i--要判断是否搬走的石头
int i = 0, now = 0;
int cnt = 0;
while (i <= size)
{
i++;
if (a[i] - a[now] < pos)
{
//搬走i处的石头
cnt++;
continue;
}
else
{
now = i;
}
}
if (cnt <= need)
return true;
else
return false;
}
int main()
{
int L, N, M;
cin >> L >> N >> M;
int* a = new int[N + 10];
memset(a, 0, sizeof(a));
for (int i = 1; i <= N; i++)
{
cin >> a[i];
}
a[N + 1] = L;
int left = 0, right = L + 1;
while (left + 1 != right)
{
int mid = (left + right) / 2;
if (check(a,N,mid,M))
{
left = mid;//左边是符合条件的
}
else
{
right = mid;
}
}
cout << left<< endl;
return 0;
}
七:路标设置---最大值最小问题
答案在(-1,L+1)之间所以面向答案验证。left=-1,right=L+1;
这里是求最大值尽可能小,所以right区间符合要求,则求最小的right值即可。
check方法是保证所有间隔距离小于等于验证Mid,如果大于则增设一个路标,cnt++;
最后如果增设数小于等于要求的K,则符合right区间,否则不符。
code:
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N = 100010;
int q[N];
bool check(int pos,int size,int need)
{
int now = 1, i = 2;
int cnt = 0;
int a[N];
memcpy(a, q,sizeof(q));
while(i<=size)
{
if (a[i] - a[now] > pos)
{
a[now] += pos;
cnt++;
}
else
{
now = i;
i++;
}
}
if (cnt > need)
return false;
else
return true;
}
int main()
{
int L, N, K;
cin >> L >> N >> K;
for (int i=1;i<=N;i++)
{
cin >> q[i];
}
int left = -1, right = L+1;
while (left + 1 != right)
{
int mid = (left + right)/2;
if (!check(mid,N,K))
{
left = mid;
}
else
{
right = mid;
}
}
cout << right << endl;
return 0;
}