再谈二分查找

再谈二分查找

“The key to performance is elegance, not brute force. Binary search is a perfect example.”
(“高性能的关键在于优雅,而非蛮力。二分查找正是典范。”) ——Brian Kernighan


二分查找,也称折半搜索,对数搜索,是用来在一个有序数组中查找某一元素的算法。

以在一个升序数组中查找一个数为例。它每次考察数组当前部分的中间元素,如果中间元素刚好是要找的,就结束搜索过程;如果中间元素小于所查找的值,那么左侧的只会更小,不会有所查找的元素,只需到右侧查找;如果中间元素大于所查找的值同理,只需到左侧查找。

因为在二分搜索过程中,算法每次都把查询的区间减半,所以对于一个长度为n的数组,至多会进行O(log(n))次查找。


基础模板

数据必须有序!且需要问题的答案具有单调性

while(l<r){
		int mid=l+r+1>>1; //对于n是有符号数的情况,当n>0时,n>>1比n/2指令数更少,速度更快;
		if(chack(mid))
		l=mid;
		else
		r=mid-1;
	}

chack()函数的编写需要根据题目灵活调整;防止出现死循环;

具体的题目特征和基本思路,在下面的例题中总结!


EKO / 砍树(模板)

[P1873 COCI 2011/2012 #5] EKO / 砍树 - 洛谷

题目描述

伐木工人 Mirko 需要砍 M M M 米长的木材。对 Mirko 来说这是很简单的工作,因为他有一个漂亮的新伐木机,可以如野火一般砍伐森林。不过,Mirko 只被允许砍伐一排树。

Mirko 的伐木机工作流程如下:Mirko 设置一个高度参数 H H H(米),伐木机升起一个巨大的锯片到高度 H H H,并锯掉所有树比 H H H 高的部分(当然,树木不高于 H H H 米的部分保持不变)。Mirko 就得到树木被锯下的部分。例如,如果一排树的高度分别为 20 , 15 , 10 20,15,10 20,15,10 17 17 17,Mirko 把锯片升到 15 15 15 米的高度,切割后树木剩下的高度将是 15 , 15 , 10 15,15,10 15,15,10 15 15 15,而 Mirko 将从第 1 1 1 棵树得到 5 5 5 米,从第 4 4 4 棵树得到 2 2 2 米,共得到 7 7 7 米木材。

Mirko 非常关注生态保护,所以他不会砍掉过多的木材。这也是他尽可能高地设定伐木机锯片的原因。请帮助 Mirko 找到伐木机锯片的最大的整数高度 H H H,使得他能得到的木材至少为 M M M 米。换句话说,如果再升高 1 1 1 米,他将得不到 M M M 米木材。

输入格式

1 1 1 2 2 2 个整数 N N N M M M N N N 表示树木的数量, M M M 表示需要的木材总长度。

2 2 2 N N N 个整数表示每棵树的高度。

输出格式

1个整数,表示锯片的最高高度。

输入输出样例 #1

输入 #1

4 7
20 15 10 17

输出 #1

15

输入输出样例 #2

输入 #2

5 20
4 42 40 26 46

输出 #2

36

说明/提示

对于 100 % 100\% 100% 的测试数据, 1 ≤ N ≤ 1 0 6 1\le N\le10^6 1N106 1 ≤ M ≤ 2 × 1 0 9 1\le M\le2\times10^9 1M2×109,树的高度 ≤ 4 × 1 0 5 \le 4\times 10^5 4×105,所有树的高度总和 > M >M >M


思路分析

很经典的模板题;直接套用模板即可;

总结出二分答案题的几个特征:

(1)求最大/最小值;

(2)答案离散(答案有有限种可能);

(3)容易判断答案是否正确。

二分答案题的基本思路:

(1)确定答案区间;

(2)在保证答案在区间内的前提下,逐步缩小区间;

(3)当区间缩小到仅包含一个可能解时,该可能解即为答案。

这里主要要注意第(2)点,就是如何缩小区间。这是二分的精华所在。也是二分搜索程序如此难写的原因


代码实现

记得先排序!!!!

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=1e7;
int a[N];
int n,m;
bool chack(int mid){
	int s=0;
	for(int i=1;i<=n;i++){
		s+=max((int)0,a[i]-mid); // 累加木头的长度
	}
	return s>=m;// 判断是否达到题目的需求
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	cin>>a[i];
	sort(a+1,a+1+n);
	int l=0,r=a[n];
	while(l<r){
		int mid=l+r+1>>1;
		if(chack(mid))
		l=mid;
		else
		r=mid-1;
	}
	cout<<l<<endl;
}

Aggressive cows G (贪心+二分)

[P1676 USACO05FEB] Aggressive cows G - 洛谷

题目描述

农夫约翰建造了一座有 n n n 间牛舍的小屋,牛舍排在一条直线上,第 i i i 间牛舍在 x i x_i xi 的位置,但是约翰的 m m m 头牛对小屋很不满意,因此经常互相攻击。约翰为了防止牛之间互相伤害,因此决定把每头牛都放在离其它牛尽可能远的牛舍。也就是要最大化最近的两头牛之间的距离。

牛们并不喜欢这种布局,而且几头牛放在一个隔间里,它们就要发生争斗。为了不让牛互相伤害。约翰决定自己给牛分配隔间,使任意两头牛之间的最小距离尽可能的大,那么,这个最大的最小距离是多少呢?

输入格式

第一行用空格分隔的两个整数 n n n m m m

第二行为 n n n 个用空格隔开的整数,表示位置 x i x_i xi

输出格式

一行一个整数,表示最大的最小距离值。

输入输出样例 #1

输入 #1

5 3
1 2 8 4 9

输出 #1

3

说明/提示

【样例解析】把牛放在 1 1 1 4 4 4 8 8 8 这三个位置,距离是 3 3 3。容易证明最小距离已经最大。

【数据范围】对于 100 % 100\% 100% 的数据, 2 ≤ n ≤ 1 0 5 2 \le n \le 10^5 2n105 0 ≤ x i ≤ 1 0 9 0 \le x_i \le 10^9 0xi109 2 ≤ m ≤ n 2 \le m \le n 2mn。不保证 x x x 数组单调递增。


思路分析

通过阅读题目,大致可以感受到一种贪心算法:从最左端开始,每隔一个单位距离 x 就放一头牛,一定是能放就放且放了就一定比不放更好,最后统计放了几头牛即可。

题目上的牛舍输入时并不是有序的;所以==先排序==!

所以我们就可以对最小的距离进行二分;如果放的牛数比题目中的总数少,那就说明距离太大了,需要缩小有边界;反之中间的区域绰绰有余,缩小左边界;

bool chack(int mid){
	int an=1,s=1;
	for(int i=2;i<=n;i++){
		if(a[i]-s>=mid) //距离大于判断的距离就放牛
		s=a[i],an++;    //更新上一只牛的位置  用an计数
	}
	return an>=m;// 牛的数量超过题目就返回1 对左边界修改
}

代码实现

记得先排序!!!!

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=1e7;
int a[N];
int p=0;int n,m;
bool chack(int mid){
	int an=1,s=1;
	for(int i=2;i<=n;i++){
		if(a[i]-s>=mid)
		s=a[i],an++;
	}
	return an>=m;
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	cin>>a[i];
	sort(a+1,a+1+n);
	int l=1,r=a[n];
	while(l<r){
		int mid=l+r+1>>1;
		if(chack(mid))
		l=mid;
		else
		r=mid-1;
	}
	cout<<l<<endl;
}

同类型练习

直接套用模板即可

P1824 进击的奶牛 - 洛谷

[P2678 NOIP 2015 提高组] 跳石头 - 洛谷

[P2855 USACO06DEC] River Hopscotch S - 洛谷


一元三次方程求解(递归做法)

题目描述

有形如: a x 3 + b x 2 + c x + d = 0 a x^3 + b x^2 + c x + d = 0 ax3+bx2+cx+d=0 这样的一个一元三次方程。给出该方程中各项的系数( a , b , c , d a,b,c,d a,b,c,d 均为实数),并约定该方程存在三个不同实根(根的范围在 − 100 -100 100 100 100 100 之间),且根与根之差的绝对值 ≥ 1 \ge 1 1。要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后 2 2 2 位。

提示:记方程 f ( x ) = 0 f(x) = 0 f(x)=0,若存在 2 2 2 个数 x 1 x_1 x1 x 2 x_2 x2,且 x 1 < x 2 x_1 < x_2 x1<x2 f ( x 1 ) × f ( x 2 ) < 0 f(x_1) \times f(x_2) < 0 f(x1)×f(x2)<0,则在 ( x 1 , x 2 ) (x_1, x_2) (x1,x2) 之间一定有一个根。

输入格式

一行, 4 4 4 个实数 a , b , c , d a, b, c, d a,b,c,d

输出格式

一行, 3 3 3 个实根,从小到大输出,并精确到小数点后 2 2 2 位。

输入输出样例 #1

输入 #1

1 -5 -4 20

输出 #1

-2.00 2.00 5.00

思路分析

题目为了让我们方便使用二分而规定在了[-100,100]的区间内;而答案的精确度只要求在0.01,所以直接暴力枚举-100.00到100.00的全部小数(最多10^5次),也不会超时;

二分做法则是先找到大致的点再不断逼近需要的精确度;

根据函数的图像可以分析出来函数的根就是函数图像和x轴的交点的横坐标;因为题目的数据都满足一定有三个根,所以也一定满足在交点的两侧的函数值异号;

所以找到找到异号的区间进行二分即可;

for(double i=-100;i<=100,c!=3;i++){ // 枚举找异号的区间
		if(jisr(i)==0){ // 恰好是交点就输出
			printf("%.2lf ",i);
			c++; // 用c记录已经输出的根的个数
			continue;
		}
		if(jisr(i)*jisr(i+1)<0){ // 找到异号区间就开始二分,提高精确度
			erff(i,i+1); 
			c++;
		}
	}

递归的模板如下(其实和while的写法差不多

void erff(double l,double r){
	if(r-l<0.001){ // 满足题目的精确度就输出
		printf("%.2lf ",r);
		return;
	}
	double ww=(l+r)/2;
	if(jisr(ww)==0){
		printf("%.2lf ",ww);
		return;
	}
	if(jisr(ww)*jisr(l)<0)
	r=ww;
	else
	l=ww;
	erff(l,r); //不断缩小范围
}

代码实现

当然这题的解法分成丰富,题解中的解法也很多,可以自行查看一元三次方程求解的题解

#include<bits/stdc++.h>
using namespace std;
double a,b,c,d;
double jisr(double x){
	return a*x*x*x+b*x*x+c*x+d;
}
void erff(double l,double r){
	if(r-l<0.001){
		printf("%.2lf ",r);
		return;
	}
	double ww=(l+r)/2;
	if(jisr(ww)==0){
		printf("%.2lf ",ww);
		return;
	}
	if(jisr(ww)*jisr(l)<0)
	r=ww;
	else
	l=ww;
	erff(l,r);
}
int main(){
	cin>>a>>b>>c>>d;
	int c=0;
	for(double i=-100;i<=100,c!=3;i++){
		if(jisr(i)==0){
			printf("%.2lf ",i);
			c++;
			continue;
		}
		if(jisr(i)*jisr(i+1)<0){
			erff(i,i+1);
			c++;
		}
	}
} 

STL的二分查找

二分的模板写着还是太麻烦了,有么有什么简单好操作的方法?

有的,兄弟有的

C++ 标准库头文件 <algorithm>

lower_bound:查找首个不小于给定值的元素的函数;(查找范围左闭区间)

upper_bound查找首个大于给定值的元素的函数;(查找范围是左开区间)

这两个函数的返回值都是地址,所以需要减去数组的首地址才能得到下标

二者均采用二分实现,所以==调用前必须保证元素有序==


序列(前缀和+二分)

[B3799 NICA #1] 序列 - 洛谷

题目描述

小 A 有一个长度为 n n n 的序列 a 1 , a 2 , … a n a_1,a_2,\dots a_n a1,a2,an。他希望支持两种操作:

  • 1 k,给序列中的每一个元素加上一个整数 k k k
  • 2,查询序列中的最大子序列和。

子序列指的是从原序列中去除某些元素(也可以不去除),但不破坏余下元素的相对位置形成的新的序列。例如,对于序列 { 2 , 3 , 4 , 5 , 6 } \{2,3,4,5,6\} {2,3,4,5,6},那么 { 2 , 3 , 4 } , { 2 , 4 , 6 } \{2,3,4\},\{2,4,6\} {2,3,4},{2,4,6} 都是它的子序列,而 { 6 , 5 , 4 } \{6,5,4\} {6,5,4} 不是。子序列可以为空,此时子序列和为 0 0 0

输入格式

第一行输入两个正整数 n , m n,m n,m,分别表示序列的长度和操作次数。

第二行输入 n n n 个正整数 a i a_i ai,表示序列的元素。

第三行开始,往下 m m m 行,每一行分别为 1 k 或者 2 的形式,含义如题意所述。

输出格式

对于每个 2 2 2 操作,输出一行一个整数表示答案。

输入输出样例 #1

输入 #1

5 5
-5 12 -7 2 8
2
1 3
2
1 4
2

输出 #1

22
31
45

说明/提示

【样例解释】

  • 第一次操作求序列中的最大子序列和,则为 12 + 2 + 8 = 22 12+2+8=22 12+2+8=22
  • 第二次操作让序列中每一个元素加上了 3 3 3。此时序列变为 − 2 , 15 , − 4 , 5 , 11 -2,15,-4,5,11 2,15,4,5,11
  • 第三次操作求序列中的最大子序列和,则为 15 + 5 + 11 = 31 15+5+11=31 15+5+11=31
  • 第四次操作让序列中每一个元素加上了 4 4 4。此时序列变为 2 , 19 , 0 , 9 , 15 2,19,0,9,15 2,19,0,9,15
  • 第五次操作求序列中的最大子序列和,则为 2 + 19 + 9 + 15 = 45 2+19+9+15=45 2+19+9+15=45

数据保证, 1 ≤ n , m ≤ 5 × 1 0 5 1 \leq n,m \leq 5\times 10^5 1n,m5×105 − 5 × 1 0 5 ≤ a i , k ≤ 5 × 1 0 5 -5\times 10^5 \leq a_i,k \leq 5\times 10^5 5×105ai,k5×105,操作仅为 1 1 1 2 2 2 操作。


思路分析

题目不难理解;存在两种操作;

  • 对于操作 1,用一个变量记录所需加上的数字之和。
  • 对于操作 2,求出序列 a 中的正整数之和(因为如果是负数则会让结果变小)

如果暴力找到大于0的项在相加会超时;所以我本就可以利用二分的函数lower_bound,找到正数项的位置再去相应的区间和;

因为是多实例,所以可以利用前缀和缩短求区间的时间;

auto w=lower_bound(a+1,a+1+n,-1*s); // 找大于-s的数,操作后大于-s的数都是正数
int ww=w-a; //返回的是地址,所以减去首地址去找下标
cout<<b[n]-b[ww-1]+(n-ww+1)*s<<endl; //输出时不要忘记1操作是增加的数

代码实现

记得先排序!!!!

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=1e7;
int a[N],b[N];
int s=0;
bool chack(int mid){
	return a[mid]+s<0;
}
signed main(){
	int n,m;cin>>n>>m;
	for(int i=1;i<=n;i++)
	cin>>a[i];
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++)
	b[i]=b[i-1]+a[i];
	while(m--){
		int op;cin>>op;
		if(op==1){
			int x;cin>>x;
			s+=x;
		}
		else{
			auto w=lower_bound(a+1,a+1+n,-1*s);
			int ww=w-a;
			cout<<b[n]-b[ww-1]+(n-ww+1)*s<<endl;
		}
	}
}

烦恼的高考志愿(模拟+二分)

P1678 烦恼的高考志愿 - 洛谷

题目背景

计算机竞赛小组的神牛 V 神终于结束了高考,然而作为班长的他还不能闲下来,班主任老 t 给了他一个艰巨的任务:帮同学找出最合理的大学填报方案。可是 v 神太忙了,身后还有一群小姑娘等着和他约会,于是他想到了同为计算机竞赛小组的你,请你帮他完成这个艰巨的任务。

题目描述

现有 m m m 所学校,每所学校预计分数线是 a i a_i ai。有 n n n 位学生,估分分别为 b i b_i bi

根据 n n n 位学生的估分情况,分别给每位学生推荐一所学校,要求学校的预计分数线和学生的估分相差最小(可高可低,毕竟是估分嘛),这个最小值为不满意度。求所有学生不满意度和的最小值。

输入格式

第一行读入两个整数 m , n m,n m,n m m m 表示学校数, n n n 表示学生数。

第二行共有 m m m 个数,表示 m m m 个学校的预计录取分数。

第三行有 n n n 个数,表示 n n n 个学生的估分成绩。

输出格式

输出一行,为最小的不满度之和。

输入输出样例 #1

输入 #1

4 3
513 598 567 689
500 600 550

输出 #1

32

说明/提示

数据范围:

对于 30 % 30\% 30% 的数据, 1 ≤ n , m ≤ 1000 1\leq n,m\leq1000 1n,m1000,估分和录取线 ≤ 10000 \leq10000 10000

对于 100 % 100\% 100% 的数据, 1 ≤ n , m ≤ 100000 1\leq n,m\leq100000 1n,m100000,估分和录取线 ≤ 1000000 \leq 1000000 1000000 且均为非负整数。


思路分析

利用lower_bound可以返回查询到的数的数组下标。那如果没有大于等于待查元素的值怎么办?函数会输出结束查询的位置,应为他并没有找到。于是我们就可以用lower_bound愉快的来写这道题了。

找到大于他的的数的位置w后,计算与第w位和第w-1位的数的差值,将较小的数记录到答案中;

s+=min(abs(a[w]-x),abs(a[w-1]-x))

特判一下如果只有1个学校;让a[2]=a[1]防止越界(不会影响结果)

在判一下如果比所有的学校的分都高(返回的w是第一个)让w++防止越界(不会影响结果)


代码实现

记得先排序!!!!

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=1e7;
int a[N];
signed main(){
	int n,m;cin>>n>>m;
	int s=0;
	for(int i=1;i<=n;i++)
	cin>>a[i];
	if(n==1)
	a[2]=a[1];
	sort(a+1,a+1+n);
	for(int i=1;i<=m;i++){
		int x;cin>>x;
		auto it=lower_bound(a+1,a+1+n,x);
		int w=it-a;
		if(w==1)
		w++;
		s+=min(abs(a[w]-x),abs(a[w-1]-x));
	}
	cout<<s;
}

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值