二分(三分)算法浅谈

一、问题引入

大家都玩过猜数字的游戏吧!

系统随机生成一个[1,n]范围内的整数a,让你猜出那个数。每次你输入一个数x,系统就会将x与a比较。若x<a则会显示“小了”;若x>a,则显示“大了”;若x与a相等,就会显示“猜中了”。

当然我们可以从小到大一个个猜,但当n很大时,我们可能就要才很久,然而怎么用最少的次数把数字猜出来呢?
我们发现,假设当前区间为[l,r],输入x,若x<a,则a在区间[x+1,r]内,否则就在[l,x]内。当我们把猜x= l + r 2 \frac{l+r}{2} 2l+r时,我们发现每次可以把区间缩小一半,这样就可以很快地猜出数来了。

二、算法简介

二分

二分算法,通常用于查找,也有“二分查找”之称,就是每次通过判断目前区间的中间值,再根据函数单调性排除一半查找范围的算法,时间复杂度一般为对数级别。注意当且仅当此函数具有单调性(不一定为严格单调性)才能用二分算法。

据说只有10%的程序员能打对二分

但是要打对二分相当困难,要注意保证它不会死循环。
比方说a[]具有单调性,要找出第一个大于x(<a[n])的元素的位置。
取中间值为a[mid]。不难发现,当a[mid]<=x时,要找的数就不会在区间[l,mid]内,那么就更新l=mid+1,再往下枚举。当l==r时,表示那个数已经被确定了。
为了保证其正确性,我们考虑mid到底向上还是向下取整。当我们向上取整时,mid=(l+r+1)/2,当区间为[1,2]时,mid=2,但可能更新r=mid=2,前后区间范围并没有变化,就会死循环,故不能向上取整。
代码就是下面这个样了。

int check(int x){
	int l=1,r=n,mid;
	while(l<r){
		int mid=(l+r)/2;  //向下取整
		if(a[mid]<=x) l=mid+1;
		else r=mid;
	}
	return l;
}

我们发现,左右边界是判断不到的,因此我们可以令其实区间为[0,n]或[1,n+1]判断无解的情况。
有时候,我们发现题目限制的某一个量与最终答案有某种单调的关系,而这个量可以通过答案倒推出来。这时候,我们就可以通过枚举答案来判断最优答案。当然我们也可以通过二分算法来“枚举”答案,称为“二分答案”,其具体过程与普通二分类似,这里不再赘述。

三分

下面简单介绍一下作为二分推广的三分算法。
三分算法被运用于严格单峰函数的极值问题。为了方便讨论,不妨设要求最大值,即极点左侧严格单调递增,右侧严格单调递减。
对于已经求出的区间范围[l,r],要求f(x)在此区间内的最大值,我们令m1= 2 l + r 3 \frac{2l+r}{3} 32l+r,m2= l + 2 r 3 \frac{l+2r}{3} 3l+2r。不妨令f(m1)<f(m2),则有下面两种情况,我们考虑区间[l,m2]和[m1,r]。
1.m1,m2均在极值点左侧,则极值在[m1,r]内不在[l,m2]内
2.m1在左侧,m2在右侧,则极值在上述两个区间内
我们发现无论哪种情况,[m1,r]内。因此,每次我们只需判断f(m1)和f(m2)大小,取不优的那个点,选出合适的区间。这样就能把枚举时间优化到对数级别。

三、经典例题

切绳子
有 N 条绳子,它们的长度分别为 Li 。如果从它们中切割出 K 条长度相同的绳子,这 K 条绳子每条最长能有多长?答案保留到小数点后 2 位(直接舍掉 2 位后的小数)。

这道题是一道经典的二分答案模板。我们发现,对于每一个固定的绳子长度,方案都是唯一的。而求解可切成长度为x的绳子的段数,我们通过O(n)枚举原来的每一条绳子,在逐个求解累计即可。

int check(double x){
	int tot=0;
	for(int i=1;i<=n;i+=1){
		tot+=floor(L[i]/x);
	}
	return tot;
}

若枚举到最大的x满足tot==k,输出x即可。我们发现,对于x1<x2,有tot1>=tot2,及tot是不上升的。根据这个性质,我们可以自然而然的想到对长度二分(这里注意要枚举到精度的后两位及10-4位)。假设最大长度为m,则时间复杂度近似为O(nlog2m)

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,k;
double l,r,mid,L[10001];
int check(double x){
	int tot=0;
	for(int i=1;i<=n;i+=1){
		tot+=floor(L[i]/x);
	}
	return tot;
}
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i+=1){
		scanf("%lf",&L[i]);
		r=max(L[i],r);
	}
	while(l+1e-4<r){
		mid=(l+r)/2;
		if(check(mid)>=k) l=mid;  //求最大值,等于k时也往大的区间看
		else r=mid;
	}
	printf("%.2lf\n",l-0.004);  //可能要注意向下取整之类的
	return 0;
}

谢谢观看

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值