算法竞赛 二分查找 / 二分答案入门指导(详细及例题)

本文深入解析了二分查找与二分答案的概念、原理及其应用,通过实例讲解了如何利用二分查找提高算法效率,以及如何通过二分答案优化枚举算法,适合算法初学者快速入门。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为了最大化时间和效率(偷懒 ), n u o y a n l i nuoyanli nuoyanli不打算给新生上课讲二分,而是选择以 p d f pdf pdf的形式给出。
对于新生来说二分这个词语肯定比较陌生(少部分做过郑轻的题的人应该看到过这个词),下面我将整个 p d f pdf pdf分成两部分给大家入门指导。

这里先提一下复杂度:

在竞赛中,一般计算机一秒能运行 5 ∗ 1 0 8 5*10^8 5108次汁算,如果题目給出的时间限制为 1 s 1s 1s,那么你选择的算法执行的汁算次数最多应该在 1 0 8 10^8 108量级オ有可能解决这个题目。
一般
Θ ( n ) Θ(n) Θ(n)的算法能解决的数据范围在 n ≤ 1 0 8 n \leq 10^8 n108
Θ ( n ∗ log ⁡ 2 n ) Θ(n *\log_2n) Θ(nlog2n)的算法能解决的数据范围在 n ≤ 1 0 6 n \leq 10^6 n106
Θ ( n ∗ n ) Θ(n*\sqrt n) Θ(nn ) 的算法能解决的数据范围在 n < 1 0 5 。 n < 10^5。 n<105
Θ ( n 2 ) Θ(n^2) Θ(n2)的算法能解决的数据范围在 n < 5000 n<5000 n<5000
Θ ( n 3 ) Θ(n^3) Θ(n3)的算法能解决的数据范围在 n < 300 n <300 n<300
Θ ( 2 n ) Θ(2^n) Θ(2n)的算法能解决的数据范围在 n < 25 n < 25 n<25
Θ ( n ! ) Θ(n!) Θ(n!)的算法能解决的数据范围在 n < 11 n < 11 n<11

以上范围仅供参考,实际中还要考虑每种算法的常数。

二分的原理:

二分二分,顾名思义,就是将查找的区间分成两半,找中间的部分,然后判断查找左半边还是右半边。
很显然,二分有一个非常重要的条件:查找元素必须有序
不然就难以判断它到底往左找还是往右找
同时,要注意二分查找二分答案在原理上相同,但二分查找用于查找元素,而二分答案更像是枚举算法的优化,在做题时不要弄混。

那么,二分到底优秀在哪呢?

二分查找

上面介绍到,评价一个算法,我们可以从时间复杂度和空间复杂度来判断:

二分查找的空间复杂度: Θ ( n ) Θ(n) Θ(n),和一般算法相同
二分查找的时间复杂度: Θ ( log ⁡ 2 2 n ) Θ(\log_22n) Θ(log22n),从这里就可以看出二分查找优秀了很多。

比如我们有105个元素,一般的方法在最坏情况下应查找105次,如果是二分的话,只需要 log ⁡ 2 210 = 7 \log_2210=7 log2210=7次即可。

以在一个升序数组中查找一个数为例:

它每次考察数组当前部分的中间元素,如果中间元素刚好是要找的,就结束搜索过程;
如果中间元素小于所查找的值,那么左侧的只会更小,不会有所查找的元素,只需要到右侧去找就好了;
如果中间元素大于所查找的值,同理,右侧的只会更大而不会有所查找的元素,所以只需要到左侧去找。
在二分搜索过程中,每次都把查询的区间减半,因此对于一个长度为 n n n a r r arr arr数组查找等于 k e y key key的数,至多会进行 log ⁡ 2 2 n \log_22n log22n 次查找。

第一步:排序(使得数组具有单调性)

做二分查找题,在题目没有明确表示的情况下,我们得先进行预处理
即给数组排序

sort(arr+1,arr+n+1);
第二步:确定边界

开始二分查找前我们要确定查找的边界,即这个数可能在第几到第几的范围内出现。
很显然,这题我们并不能直接圈定一个范围,那么我们就无脑直接将范围设成整个数组
我们以 l l l表示左边界 r r r表示右边界

l=1,r=n;

显然,左右边界在查找过程中会发生改变,所以接下来有一个问题,见下文。
预处理结束边界也确定了,我们就可以开始查找了。

第三步:开始查找

我们之前提到了,分左右之后找中间的部分,也就是最中间的那个。
查找肯定不能一次就结束啊,所以我们要用循环。
设置变量 m i d mid mid为当前查找的位置。
一般用 w h i l e while while循环:

l=1,r=n;
while(l<=r) {
    mid=l+r >> 1;
}

这里的 > > 1 >> 1 >>1代表二进制右移一位,即 / 2 1 /2^1 /21,占用时间较少。
:位运算的优先级低于加减乘除模(所以我没有用括号
接下来我们要进一步判断是向左找还是向右找。
设置 b o o l bool bool类型的变量 f l a g flag flag代表有没有找到,初始为 f a l s e false false

if(a[mid]==key) {//找到了就弹出循环
    flag=true;
    break;
}
if(arr[mid]>key) {
	r=mid-1;
}
else {
	l=mid+1;
}

很好理解吧, 当前元素比查找元素大则我们在查找元素的右边,反之亦然。
那么有新生会注意到,为什么 l l l r r r + 1 , − 1 +1,-1 +11呢?
假定此时 l = 2 l=2 l=2 r = 3 r=3 r=3,得 m i d = 2 mid=2 mid=2 ,如果此时不是查找元素的话,那么如果当前元素比查找元素小,那么我们调整 l l l 的值。
如果不 + 1 +1 +1的话会发生什么呢? l l l 再次被赋值为 2 2 2,则进入无限循环,所以我们需要 + 1 +1 +1 − 1 -1 1来改变我们要查找的值的可能存在的区间。
则核心代码如下:

sort(arr+1,arr+n+1);
bool flag=false;
int l=1,r=n;
while(l<=r) {
	int mid=l+r >> 1;
	if(arr[mid]==key) {
		flag=true;
		break;
	}
	if(arr[mid]>key) {
		r=mid-1;
	} else l=mid+1;
}

接下来我们举个栗子
假定 a r r arr arr数组为:( 1 1 1为起点

1 2 4 5 7 8 9 11

我们来找 4 4 4这个元素
程序开始
l = 1 , r = 8 , m i d = 4 , a r r [ m i d ] > 4 , r = m i d − 1 = 3 l=1 , r=8 , mid=4 , arr[mid]>4 , r=mid-1=3 l=1,r=8,mid=4,arr[mid]>4,r=mid1=3
l = 1 , r = 3 , m i d = 2 , a r r [ m i d ] < 4 , l = m i d + 1 = 3 l=1 , r=3 , mid=2 , arr[mid]<4 , l=mid+1=3 l=1,r=3,mid=2,arr[mid]<4,l=mid+1=3
l = 3 , r = 3 , m i d = 3 , a r r [ m i d ] = 4 , 二 分 查 找 成 功 , 退 出 循 环 l=3 , r=3 , mid=3 , arr[mid]=4 , 二分查找成功,退出循环 l=3,r=3,mid=3,arr[mid]=4,退
程序结束。
真的很短很快有木有?!
那么二分查找可以应用在哪里呢?
以后会用到 L I S LIS LIS L C S LCS LCS Θ ( n l o g n ) Θ(nlogn) Θ(nlogn)版就可以用到二分查找。)
接下来就是更喜闻乐见丧心病狂的二分答案

二分答案:优雅的暴力,将枚举复杂度降低

例题:Merry Christmas(本来想在这次新生赛出的,但是怕被打 )
題目链接:https://nuoyanli.com/problem/1916
二分答案入门题。
看到这道题,大多数人的想法应该是暴力枚举切段长度吧。
别说我特意试了一发暴力:(自己造的数据,自己卡自己
在这里插入图片描述
参考 代码:

#include<cstdio>
#define max(a,b) a>b?a:b
const int N = 2e5+5;
#define ll long long
ll n,k,a[N],sum=0,t;
int main() {
	scanf("%lld%lld",&n,&k);
	for(int i=1; i<=n; i++) {
		scanf("%lld",&a[i]);
		sum+=a[i];
	}
	t=sum/k;
	ll count;
	for(ll i=t; i>0; i--) {
		count=0;
		for(ll j=1; j<=n; j++) {
			count+=a[j]/i;
			if(count>=k) {
				printf("%lld\n",i);
				return 0;
			}
		}
	}
	printf("0\n");
	return 0;
}

你可以转头一看,数据范围——
在这里插入图片描述
暴力,不存在的!
于是我们便要用到在答案可能在的范围中二分查找我们的答案了。
对这道题来说,答案的范围就是从1到 最长的原木长度。
于是,我们设 l = 1 , r = m a x n l=1 , r=maxn l=1,r=maxn
好了,范围解决了,接下来又面临一个问题了,怎样判断答案可不可行呢?
没办法了,只能暴力了。
每根原木扫一遍,算出能切多少根,再加起来和 k k k比较一下即可
判断的问题解决了,怎么判断下一步搜索的范围呢?
很显然,如果切得太少,那么长度太长, r = m i d − 1 r=mid-1 r=mid1
如果切得太多,虽然达到了要求,此时我们可以记录答案,但长度可能太短,可能有更优解,那么 l = m i d + 1 l=mid+1 l=mid+1
在这里插入图片描述

二分详细代码如下:
#include<cstdio>
#define max(a,b) a>b?a:b
const int N = 2e5+5;
int n,k,a[N],l=1,r=0;
bool check(int x) {
	int ans=0;//设置初值
	for(int i=1; i<=n; ++i)ans+=a[i]/x;//计算能切多少
	return ans>=k;//如果切够了返回true,不够返回false
}
void solve() {
	scanf("%d%d",&n,&k);
	for(int i=1; i<=n; ++i)scanf("%d",&a[i]),r=max(a[i],r);
	while(l<=r) {
		int mid=l+r>>1;
		if(check(mid))l=mid+1;
		else r=mid-1;
	}
	printf("%d\n",r);
}
int main() {
	solve();
	return 0;
}

写的有点多,希望对大家有所帮助,有问题的欢迎24小时问我。( / [ 斜 眼 笑 ] /[斜眼笑] /[]

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nuoyanli

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值