二分查找的理解,以及多道例题的解析

为了照顾不熟悉二分的人,我先在前面啰嗦几句二分查找的实现和原理;

先说原理,首先二分查找只适用于单调区间,非单调区间是不能用的,所以一般会要求题目有“单调性”。【这里的单调性很抽象,本菜鸟觉得需要做一些题才能有感觉】

先举一个例子,给你一个单调递增的数组,然后给你一个数,让你查找一下该数在数组中的下标,如果不存在,那么输出-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();

}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值