为了照顾不熟悉二分的人,我先在前面啰嗦几句二分查找的实现和原理;
先说原理,首先二分查找只适用于单调区间,非单调区间是不能用的,所以一般会要求题目有“单调性”。【这里的单调性很抽象,本菜鸟觉得需要做一些题才能有感觉】
先举一个例子,给你一个单调递增的数组,然后给你一个数,让你查找一下该数在数组中的下标,如果不存在,那么输出-1;
我们用两个指针讲答案所在的范围不断缩小,直到找到答案,二分的时间复杂度非常优秀,为o(log n) n为数组中元素的个数;
先放一个本菜鸟经常用的二分模板,可能和部分人的模板不同,但是醉翁之意不在酒;
int l = 0, r = n;
while (l < r) {
int mid = l + r >> 1;
if (mid >= x) r = mid;
else l = mid + 1;
}
还有一个
int l = 0, r = n;
while (l < r) {
int mid = l + r + 1 >> 1;
if (mid <= x) l = mid;
else r = mid - 1;
}
为什么放两个模板呢?因为两个模板的意义是不同的,第一个模板是求出数组中小于x的最大值的位置,第二个模板是求出数组中大于x的最小值的位置,一些题目是需要这两个模板协同配合的;
写二分最害怕的是什么,当属死循环,mid的公式和对应的 l 、r 的变化不同时会死循环,这里本菜鸟有一个辨别的好方法,一般死循环都是l和r相邻时造成的,此时我们要记住,当l和r相邻时,l+r>>1 于l相等,l+r+1>>1 与r相等,我们只需要将l和相邻的特殊情况代入我们的程序,然后人眼走一遍,看是否会发生区间不变的情况;
说了这么多,还是来上几道题吧,难易程度逐渐上升;
由于这个测试网站是收费的,所以本菜鸟就不放出来了;
这道题目就是一道裸题,直接套用上面两个模板,算出两端的下标就行了;
那么我就直接将代码和注释放出来了;
#include<iostream>
using namespace std;
int a[100010];
int main() {
int n, qua;//n表示数据个数,qua为询问次数
cin >> n >> qua;
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
while (qua--) {
int x, l = 0, r = n - 1; cin >> x;//l、r为两端指针,初始时指针的范围为整个数组
while (l < r) {//求出小于等于x的最大值的位置,因为有元素重复,所以为第一个x的位置
int mid = (l + r) >> 1;
if (a[mid] >= x) r = mid;
else l = mid + 1;
}
if (a[l] != x) {//如果x不存在与数组中,直接输出-1 -1即可
printf("-1 -1\n");
}
else {
cout << l << ' ';
l = 0, r = n - 1;
while (l < r) {//求出大于等于x的最小值的位置,因为有元素重复,所以为最后一个x的位置
int mid = (l + r + 1) >> 1;
if (a[mid] <= x) l = mid;
else r = mid - 1;
}
cout << l << endl;
}
}
}
来一道难度提升的题目
P2440 木材加工 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这里就和上面的裸题不一样了,我们这里就开始涉及到二分的核心“check()"函数了,check函数是判断mid是否能让原木切出7根,如果能大于等于7根,那么我们就让mid增加,即让l=mid,如果小于7根我们就让mid减小,即r=mid,如此逼近,当 r==l 时 l 和 r即为答案;
有思路后就直接放代码了;
#include<iostream>
using namespace std;
#define ll long long
const int N = 1000010;
int a[N];
ll n, k;
bool check(ll mid) {
ll sum=0;
for (int i = 0; i < n; i++)
sum += a[i] / mid;//计算一颗原木能砍成几块
if (sum < k) return false;
else return true;
}
ll find() {
ll l = 0, r = 0;
for (int i = 0; i < n; i++) r += a[i];
r /= n, r++; //初始时只要保证r比标准答案大即可
while (l < r) {
ll mid = r + l+1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
int main() {
cin >> n >> k;
for (int i = 0; i < n; i++)
cin >> a[i];
cout << find();
}
那接下来再来一道难度上升的题目;
说实话,其实还有一道题目我准备发出来,但是当本菜鸟写那道题目的时候出现了意外,卡住了,搞得本菜鸟没心情,所以就直接放代码和注释了,具体思路是假定最短距离为mid,然后计算在这个最短距离下需要拿走的石头,然后以这个做判断,以此缩减范围;
#include<iostream>
using namespace std;
#define ll long long
const int N = 100010;
ll a[N];
ll lm, n, m;
bool check(ll mid) {
ll now = 0, sum = 0;//now记录当前在第几块石头上,sum记录拿掉几块石头
for (int i = 0; i <= n+1; i++) {
if (a[i] - a[now] >= mid||i==n+1) {
sum += i - now - 1;
now = i;
if (sum > m) return false;//拿的石头大于m时我们需要将mid减小
}
}
return true;
}
ll find() {
ll l = 0, r = lm;
while (l < r) {
ll mid = l + r+1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
int main() {
cin >> lm >> n >> m;
a[0] = 0, a[n+1] = lm;//将起点和终点都放入数组,减少特殊判断
for (int i = 1; i <= n; i++)
cin >> a[i];
cout << find();
}