三、二分法

二分查找

区间选择部分为后续新补充的
用sort排序大家应该都会,而排序的目的就是方便我们查找。试想一下,如果有 1 0 6 10^6 106个杂乱的数,你要从中找到12345,那只能挨个找,但是如果这些数字像字页码一样是有序的,我们就可以估计12345大概在哪个位置然后在那个范围内找。那么这个范围又该怎么找呢?有一种猜数游戏相信大家都知道,你在1~1000中选一个数,我对你进行不超过10次的询问就能直到你猜的数是多少。具体操作是我先问你是不是500,如果大了,我就猜是不是250(1 到 499 的中间数),然后以此类推,最终只需要 log ⁡ 2 1000 \log_2{1000} log21000次,如果是1~10000呢,那只要14次。这便是猜数游戏的核心,折半查找。

二分查找(在数组中实现)
int b_search(int *arr, int left, int right, int aim){
	while(left < right){
		int mid = left + (right - left) / 2;
		//可写作:mid = (left + right) / 2;
		if(arr[mid] == aim)return m;
		else if(arr[mid] > aim)right = mid;
		else left = mid + 1;
	}
	return -1;
}

尝试过上面的代码之后会发现,如果数组中有很多个值都和aim一样,那上面的程序会返回最中间aim的下标。那我们怎么样才能使出现这种情况时,返回第一个aim的下标呢?

二分查找(求下界)
int b_search(int *arr, int left, int right, int aim){
	while(left < right){
		int mid = left + (right - left) / 2;
		if(arr[mid] >= aim)right = mid;
		else left = mid + 1;
	}
	return right;
}

我们分析一下这段代码,

  • arr[mid] == aim 时,至少找到了一个值,可能左边还有,此时区间变为 [ l e f t , m i d ] [left, mid] [left,mid]
  • arr[mid] > aim 时,一个合适的都没有,得往左找,此时区间变成 [ l e f t , m i d ] [left, mid] [left,mid]
  • arr[mid] < aim 时,一个合适的都没有,得往右找,此时区间变成 [ m i d + 1 , r i g h t ] [mid + 1, right] [mid+1,right]
    合并得到 a r r [ m i d ] > = a i m 时,新区间为 [ l e f t , m i d ] arr[mid] >= aim 时,新区间为 [left, mid] arr[mid]>=aim时,新区间为[left,mid] a r r [ m i d ] < a i m 时,新区间为 [ m i d + 1 , r i g h t ] arr[mid] < aim 时,新区间为[mid + 1, right] arr[mid]<aim时,新区间为[mid+1,right],这里会有一个潜在危险:如果 [x, m] 或者 [mid + 1, right] 与原区间相同,则会出现死循环。但是这种情况不会发生,可以自行思考。

类似的,我们如果把 i f ( a r r [ m i d ] > = a i m ) r i g h t = m i d 改为: i f ( a r r [ m i d ] < = a i m ) l e f t = m i d + 1 if(arr[mid] >= aim)right = mid 改为:if(arr[mid] <= aim)left = mid + 1 if(arr[mid]>=aim)right=mid改为:if(arr[mid]<=aim)left=mid+1;即可得到最后出现的aim了。
此时你就获得了属于你自己写出来的 upper_bound 和 lower_bound 函数了。

区间选择

⼆分查找涉及的很多的边界条件,逻辑⽐较简单,但就是写不好。例如到底是 w h i l e ( l e f t < r i g h t ) while(left < right) while(left<right) 还是 w h i l e ( l e f t < = r i g h t ) while(left <= right) while(left<=right) ,到底是 r i g h t = m i d right = mid right=mid呢,还是要 r i g h t = m i d − 1 right = mid - 1 right=mid1呢?⼤家写⼆分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在⼆分查找的过程中,保持不变量,就是在 w h i l e while while寻找中每⼀次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写⼆分法,区间的定义⼀般为两种,左闭右闭即 [ l e f t , r i g h t ] [left, right] [left,right],或者左闭右开即 [ l e f t , r i g h t ) [left, right) [left,right)。下⾯我⽤这两种区间的定义分别讲解两种不同的⼆分写法。

第⼀种写法

我们定义 t a r g e t target target是在⼀个在左闭右闭的区间⾥,也就是 [ l e f t , r i g h t ] [left, right] [left,right](这个很重要⾮常重要)。区间的定义这就决定了⼆分法的代码应该如何写,因为定义 t a r g e t target target [ l e f t , r i g h t ] [left, right] [left,right]区间,所以有如下两点:

  • w h i l e ( l e f t < = r i g h t ) while (left <= right) while(left<=right) 要使⽤ < = <= <= ,因为 l e f t = = r i g h t left == right left==right是有意义的,所以使⽤ < = <= <=
  • i f ( a r r [ m i d ] > t a r g e t ) if (arr[mid] > target) if(arr[mid]>target) r i g h t right right 要赋值为 m i d − 1 mid - 1 mid1,因为当前这个 a r r [ m i d ] arr[mid] arr[mid]⼀定不是 t a r g e t target target,那么接下来要查找的左区间结束下标位置就是 m i d − 1 mid - 1 mid1
    例如在数组: [ 1 , 2 , 3 , 4 , 7 , 9 , 10 ] [1,2,3,4,7,9,10] [1,2,3,4,7,9,10]中查找元素 2 2 2,如图所示:![[Pasted image 20240719203611.png]]
int search(int *arr, int target, int len) { // len表示数组有多少元素
	int left = 0;
	int right = len - 1; // 定义target在左闭右闭的区间⾥,[left, right]
	while (left <= right) { // 当left==right,区间[left, right]依然有效,所以⽤ <=
		int mid = left + (right - left) / 2; // 防⽌溢出 等同于(left + right)/2
		if (arr[mid] > target) {
			right = mid - 1; // target 在左区间,所以[left, mid - 1]
		 } 
		 else if (arr[mid] < target) {
			left = mid + 1; // target 在右区间,所以[mid + 1, right]
		 } 
		 else { //arr[mid] == target
			return mid; //数组中找到⽬标值,直接返回下标
		 }
	 }
	// 未找到⽬标值
	return -1;

第二种写法

如果说定义 t a r g e t target target是在⼀个在左闭右开的区间⾥,也就是 [ l e f t , r i g h t ) [left, right) [left,right) ,那么⼆分法的边界处理⽅式则截然不同。有如下两点:

  • w h i l e ( l e f t < r i g h t ) while (left < right) while(left<right),这⾥使⽤ < < < ,因为 l e f t = = r i g h t left == right left==right在区间 [ l e f t , r i g h t ) [left, right) [left,right)是没有意义的
  • i f ( a r r [ m i d ] > t a r g e t ) if (arr[mid] > target) if(arr[mid]>target) r i g h t right right 更新为 m i d mid mid,因为当前 a r r [ m i d ] arr[mid] arr[mid]不等于 t a r g e t target target,去左区间继续寻找,⽽寻找区间是左闭右开区间,所以 r i g h t right right更新为 m i d mid mid,即:下⼀个查询区间不会去⽐较 a r r [ m i d ] arr[mid] arr[mid]
    在数组: [ 1 , 2 , 3 , 4 , 7 , 9 , 10 ] [1,2,3,4,7,9,10] [1,2,3,4,7,9,10]中查找元素 2 2 2,如图所示:(注意和⽅法⼀的区别)
    ![[Pasted image 20240719205035.png]]
int search(int *arr, int target, int len) {
	int left = 0;
	int right = len; // 定义target在左闭右开的区间⾥,即:[left, right)
	while (left < right) { // 因为left == right的时候,在[left, right)是⽆效的空间,所以使⽤ <
		int mid = left + ((right - left) >> 1);
		if (arr[mid] > target) {
			right = mid; // target 在左区间,在[left, mid)中
		}
		 else if (arr[mid] < target) {
			 left = mid + 1; // target 在右区间,在[middle + 1, right)中
		}
		else { // arr[mid] == target
			return mid; // 数组中找到⽬标值,直接返回下标
		}
	}
	// 未找到⽬标值
	return -1;
}

二分答案

初中常见的一种方案题如下:篮球m元一个,足球n元一个(n > m),篮球足球一共要买10个,购买的总费用是w元,请你找出最合适的购买方案。这个题目由于数据量非常小,我们依次枚举篮球的数量计算,即可得出答案,但是如果篮球足球一共买一亿个呢?还能依次枚举吗?显然是不能的。但是我们发现篮球的个数是线性的,这样的话我们就可以对篮球的个数进行二分,然后由篮球的个数得出足球的个数,并计算出此时所需的费用。如果此时费用低于总费用,说明足球可以多买一些,于是二分区间变成原区间的左半,反之则变成右半。

二分答案的模板
	bool cmp(T&nums){
		int answer;
		//check nums to get the answer
		return compare answers and criteria
	}
	int main(){
		int l, r, ans;
		while(l < r){
			int mid = (l + r) / 2;
			if(cmp(mid)){
				ans = mid;
				...
			}
			else{
				...
			}
		}
		cout << ans;
		return 0;
	}

浮点二分

由于计算出小数时会有精度损,所以我们不能用常规的二分思维来对小数进行二分,所以我们一般会设置一个精度范围(根据题目要求设置),如果误差在这个范围内,那我们就能接受这个答案,否则就可以继续二分。

浮点二分模板
	const doubel esp = 1e-5;
	int main(){
		while(r - l > esp){
			...
		}
	}

1、查找

输入 n n n 个不超过 1 0 9 10^9 109 的单调不减的(就是后面的数字不小于前面的数字)非负整数 a 1 , a 2 , … , a n a_1,a_2,\dots,a_{n} a1,a2,,an,然后进行 m m m 次询问。对于每次询问,给出一个整数 q q q,要求输出这个数字在序列中第一次出现的编号,如果没有找到的话输出 − 1 -1 1 1 ≤ n ≤ 1 0 6 1 \leq n \leq 10^6 1n106 0 ≤ a i , q ≤ 1 0 9 0 \leq a_i,q \leq 10^9 0ai,q109 1 ≤ m ≤ 1 0 5 1 \leq m \leq 10^5 1m105

输入格式

第一行 2 2 2 个整数 n n n m m m,表示数字个数和询问次数。
第二行 n n n 个整数,表示这些待查询的数字。
第三行 m m m 个整数,表示询问这些数字的编号,从 1 1 1 开始编号。

样例输入 #1
11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6
样例输出 #1
1 2 -1

answer:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
typedef long long ll;
ll arr[N];
int main(){
    int n, m, x;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)cin >> arr[i];
    while(m--){
        cin >> x;
        int l = 1, r = n;
        while(l < r){
            int mid = (l + r) / 2;
            if(arr[mid] < x){
                l = mid + 1;
            }
            else r = mid;
        }
        if(arr[r] == x)cout << r;
        else cout << -1;
        cout << " ";
    }
	return 0;
}

2、估分

现有 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 表示学生数。
tips: 1 ≤ n , m ≤ 100000 1\leq n,m\leq100000 1n,m100000,估分和录取线 ≤ 1000000 \leq 1000000 1000000 且均为非负整数。
第二行共有 m m m 个数,表示 m m m 个学校的预计录取分数。第三行有 n n n 个数,表示 n n n 个学生的估分成绩。

样例输入 #1
4 3
513 598 567 689
500 600 550
样例输出 #1
32

题目要我们给每个学生找到一个和他分数最近的学校分数,但是学生分数不一定会和学校分数线一模一样,所以不能简单的套用二分模板。试想一下,学生分数 S 和某学校分数线 L[i] 会有什么样的关系。

  • S = L[i]
  • L[i - 1] < S < L[i]
  • S < L m i n L_{min} Lmin ,或者 S > L m a x L_{max} Lmax
    第一种情况是最好的,不满意度为0;第二种情况的话,用 S 和两学校分别计算不满意度,取差值较小的;第三种情况的话只能取最差/好的学校了。因为第三种为特使情况,所以要单独判别一下。
    answer:
/*  ~a 表示 a != -1   */
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
int arr[N];
long long ans = 0;
int x;
int n, m;
bool cmp(int index){
    return x ==  arr[index] && x <= arr[index + 1];
}
void solve(){
    cin >> x;
    int l = 1, r = n;
    if(x <= arr[l]){ans += arr[l] - x; return ;}
    else if(x >= arr[r]) {ans += x - arr[r]; return ;}
    while(l < r){
        int mid = (l + r) / 2;
        if(cmp(mid)){
            r = mid;
        }
        if(arr[mid] < x){
            l = mid + 1;
        }
        else{
            r = mid;
        }
    }
    ans += min(x - arr[r], arr[r + 1] - x);
}
int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++)cin >> arr[i];
    sort(arr + 1, arr + 1 + n);
    while(m--){
        solve();
    }
    cout << ans;
    return 0;
}

这是一个很好的二分答案的题。

3、木材加工

木材厂有 n n n 根原木,现在想把这些木头切割成 k k k 段长度 l l l 的小段木头(木头有可能有剩余)。
当然,我们希望得到的小段木头越长越好,请求出 l l l 的最大值。
木头长度的单位是 cm \text{cm} cm,原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。
例如有两根原木长度分别为 11 11 11 21 21 21,要求切割成等长的 6 6 6 段,很明显能切割出来的小段木头长度最长为 5 5 5
tips: 1 ≤ n ≤ 1 0 5 1\le n\le 10^5 1n105 1 ≤ k ≤ 1 0 8 1\le k\le 10^8 1k108 1 ≤ L i ≤ 1 0 8 ( i ∈ [ 1 , n ] ) 1\le L_i\le 10^8(i\in[1,n]) 1Li108(i[1,n])

输入格式

第一行是两个正整数 n , k n,k n,k,分别表示原木的数量,需要得到的小段的数量。
接下来 n n n 行,每行一个正整数 L i L_i Li,表示一根原木的长度。

输出格式

仅一行,即 l l l 的最大值。
如果连 1cm \text{1cm} 1cm 长的小段都切不出来,输出 0

样例输入 #1
3 7
232
124
456

样例输出 #1

114

这是一个典型的二分答案的题,分界条件是在切出来的木头块数符合要求的情况下,让每块最长。所以我们要二分的是每块木头的长度,计算在此长度下,能分出多少块木头,如果分多了,则可以每块更长一点,如果分出的木块数量不够,则要减短每块的长度。

typedef long long ll;
const int N = 1e5 +7;
ll t[N];
ll n, k, ans, maxx;
bool check(ll d){
	ll cnt = 0;
	for(int i = 1; i <= n; i++){
		cnt += t[i] / d;
	}
	return cnt >= k;
}
int main(){
	cin >> n >> k;
	for(int i = 1; i <= n; i++){
		cin >> t[i];
		maxx = max(maxx, t[i]);
	}
	ll l = 0, r = maxx, mid;
	while(l <= r){
		mid = (l + r) >> 1;
		if(mid == 0){
			ans = 0;break;
		}
		if(check(mid)){
			ans = mid;
			l = mid + 1;
		}
		else r = mid - 1;
	}
	cout << ans;
	return 0;
}

4、一元三次方程的解

有形如: 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 位。

输入格式

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

输出格式

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

样例输入 #1
1 -5 -4 20
样例输出 #1
-2.00 2.00 5.00

由题意,我们应该先确定零点在哪个区间内:f(x - 1) * f(x) <= 0;然后再在 [x - 1,x] 内二分找到根。当找到三个根时,我们就不需要继续找下去了。由于题目要求保留两位小数,所以我们精度设置为0.001。
answer:

#include<bits/stdc++.h>
using namespace std;
const double esp = 0.001;
double a, b, c, d, e;
double func(double x){
    return a*x*x*x + b*x*x + c*x + d;
}
int main(){
    int cnt = 0;
    cin >> a >> b >> c >> d;
    for(double i = -100; i < 100 && cnt < 3; i++){
        if(func(i) == 0){printf("%.2lf ",i);cnt++;}
        if(func(i) * func(i + 1) >= 0)continue;
        double l = i, r = i + 1;
        while( l < r - esp){
            double mid = (r + l) / 2;
            if(func(mid) * func(r) < 0){
                l = mid;
            }
            else r = mid;
        }
        printf("%.2lf ",r);cnt++;
    }
	return 0;
}
  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

fanxinfx2

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

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

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

打赏作者

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

抵扣说明:

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

余额充值