洛谷-二分算法

  • 二分查找

一般来说,二分查找都是在有序的区间中查找目标值,每次查找去掉不符合条件的一半区间,直到找到答案(整数二分)或者与答案相近的值(浮点二分)。

整数二分模板

//模板一:向左查找,比如可以查找第一个大于等于目标的值

while(l < r){
    int mid = l + r >> 1;
    if(check(mid)) r = mid;  //check函数是判断mid是否合法,也就是看是否满足条件
    else l = mid + 1;
}


//模板二:向右查找,比如可以查找最后一个小于等于目标的值

while(l < r){
    int mid = l + r + 1 >> 1;  //注意模板这里要加一,否则会死循环
    if(check(mid)) l = mid;
    else r = mid - 1;
}

浮点二分模板

while(r - l > 1e-7){   //因为是浮点数,需要有精度保证
    int mid = l + r >> 1;  //浮点数不存在相除取整的情况,因此浮点二分不用加一或减一
    if(check(mid)) l = mid;  
    else r = mid;
}

浮点二分比整数二分稍简单的一点是浮点数不存在相除取整的情况,因此浮点二分不用加一或减一,不用考虑边界。另外,浮点二分的while循环条件一般是根据题目的精度要求来定的,通常是题目要求的精度后移两位,比如要求保留小数点后五位,可以把while循环的条件设为r - l > 1e-7。

例1

​​​​​​P2249 【深基13.例1】查找

这道题就是经典的二分查找例题,输入单调不减的数组,再给定一个数字q,查找q在数组中第一次出现的位置并输出,如果没有找到就输出-1。

一般来说,题目如果要求出目标值第一次出现的位置,就是找大于等于它的第一个数的下标,就用模板一,相反地,如果是最后一次出现的位置,就是找小于等于它的最后一个值的下标,用模板二。当然了,模板不是唯一的,选一个自己理解起来比较容易的就行。

代码实现:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n, m, q, num[N];

int main(){
	scanf("%d%d",&n, &m);
	for(int i = 1; i <= n; i ++){
    	scanf("%d",&num[i]);
    }
    while(m --){
    	scanf("%d", &q);   //找大于等于q的第一个位置
    	int l = 1, r = n;
    	while(l < r){
    		int mid = (l + r)/2;
    		if(num[mid] >= q) r = mid;  //如果大于等于q,查找左半区间
    		else l = mid + 1;  //否则,查找右半区间,此时mid不符合条件,故向后移一位
			}
		if(num[l] != q) cout << "-1" << " ";   //查找结束,如果剩下的元素不等于q,说明数组中没有q,输出-1
		else    cout << l << " ";
		}
		return 0;
	}

 另外,STL中有专门的函数:lower_bound(frist, last, q),在[frist, last)中查找第一个大于等于q的地址;upper_bound(frist, last, q),在[frist, last)中查找第一个大于q的地址,注意这里的返回值都是地址,如果输出下标的话,还得减去num。如果数组中的元素都小于q,则返回last;binary_bound(frist, last, q),在[frist, last)中查找q,若有返回true, 否则返回false。这三种函数查找的范围必须是有序的。

例2

P1102 A-B 数对

这道题要求计算出所有的A - B = C的数对的个数,移项,A  = B + C,那么我们只需要查找A的个数就可以了,怎么找呢,可以发现大于A的数减去大于等于A的第一个数就是A的个数了。

代码实现:

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n, c, a[N];
long long ans;

int main(){
    scanf("%d%d", &n, &c);
    for(int i = 1; i <= n; i ++)
         scanf("%d", &a[i]);
    sort(a + 1, a + n + 1);
    for(int i = 1; i <= n; i ++)
        ans += upper_bound(a + 1, a + n + 1, a[i] + c) - lower_bound(a + 1, a + n + 1, a[i] + c) ;
    cout << ans << endl;
    return 0;
}

当然,可以判断B =A - C的个数, 和上面一样的思路,也可以用map映射来实现:

#include <bits/stdc++.h>
using namespace std;
map<int, int> p;    //用map映射来存储A-C的个数,也就是B的个数,累加起来就是答案 
const int N = 2e5 + 10;
int n, q[N], c;
long long ans;

int main(){
scanf("%d%d", &n, &c);
    for(int i = 1; i <= n; i ++){
         scanf("%d", &q[i]);
         p[q[i]] ++;
     }
    for(int i = 1; i <= n; i ++)
        ans += p[q[i] - c];
    cout << ans << endl;
    return 0;
}

例3

数的三次方根(浮点二分)

思路很简单,如题意,找一个数的三次方根

代码实现:

#include <bits/stdc++.h>
using namespace std;
double n;

int main(){
	scanf("%lf", &n);
	//double l = min(-1.0, n), r = max(1.0, n);  //这种也可以
	double l = 0, r = max(1.0, n);
	if(n < 0){
	    r = min(-1.0, n);
	    swap(l, r);
	}
	while(abs(r-l) >1e-8){  //精度比题目要求的大两位
		double mid = (l + r )/2;
		if((mid * mid * mid) >=n){
			r = mid ;
		}
		else{
			l = mid;
		}
	}
	printf("%.6f", l);

	return 0;
} 
	

这道题要注意的一点是当数的范围是[0, 1]时,比如0.001的三次方根是0.1,也就是说r不再是它本身,而是1,所以我们可以将r设为max(1.0, n)。[-1, 0]同理。当然了,我们也可以直接将l设为-10000,r设为10000。

例4

烦恼的高考志愿

要求学校的预计分数线和学生的估分相差最小,什么样就相差最小了呢?当然是和估分最接近的分数线和它相差最小!也就是最后一个小于估分的分数线或者第一个大于等于估分的分数线,此时我们就可以套用模板,既然不知道具体是哪个相差最小,那我们可以都找出来比较一下。

代码实现:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int m, n, a[N], b[N];
long long ans;

int main(){
	scanf("%d%d", &m, &n);
	for(int i = 1; i <= m; i ++){
		scanf("%d", &a[i]);
	}
	for(int i = 1; i <= n; i ++){
		scanf("%d", &b[i]);
	}
	sort(a + 1, a + m + 1);
	for(int i = 1; i <= n; i ++){
		int l = 1, r = m;
		while(l < r - 1){  
			int mid = l + r + 1 >> 1;
			if(a[mid] >= b[i]) r = mid;
			else l = mid;  //注意这里是不加一的,如果加一的话,有的结果会被跳过
		} 
		ans += min(abs(b[i] - a[l]), abs(a[r] - b[i]));
	}
	cout << ans << endl;
	return 0; 
} 

 也可以用另外一种方法做,思路是一样的:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int m, n, a[N], b[N];
long long ans;

int main(){
	scanf("%d%d", &m, &n);
	for(int i = 0; i < m; i ++){
		scanf("%d", &a[i]);
	}
	for(int i = 0; i < n; i ++){
		scanf("%d", &b[i]);
	}
	sort(a, a + m);
	for(int i = 0; i < n; i ++){
		int l = 0, r = m - 1;
        while(l < r){
            int mid = (l + r ) >> 1;
            if(a[mid] >= b[i]) r = mid;
            else l = mid + 1;
    }
        if(a[0] >= b[i]) ans += a[0] - b[i];  //此时要特判一下,如果b[i]恰好是0,如果不特判的话,比出来的最小的差是0,显然不对,如果查找的是小于等于就不用特判
        else
            ans += min(abs(a[l] - b[i]), abs(a[l - 1] - b[i]));
    }
    cout << ans << endl;
    return 0;
}
  • 二分答案

二分答案和二分查找的不同点在于,二分查找是直接查找,而二分答案会稍微复杂一点,先找出一个单调的答案区间,根据题目所给条件写出的check函数去二分这个答案区间。

例1

一元三次方程求解

单调的答案区间:[-100, 100],还有一个隐含条件是跟与根之间的差的绝对值大于等于1,所以可以直接在每个长度为1的小区间内查找。

代码实现:

#include <bits/stdc++.h>

using namespace std;
int main(){
	double a, b, c, d;
	scanf("%lf%lf%lf%lf", &a, &b, &c, &d);

	for(int i = -100; i <= 100; i ++){ 
		double l = i, r= i + 1;
		double x = a * l * l * l + b * l * l + c * l + d;
		double xr = a * r * r * r + b * r * r + c * r + d;
		if(x == 0) {
            printf("%.2lf ", l);  //如果i是根,直接输出,并查找下一个区间,因为根与根之间的差最少是1
            continue;   
        }
		else if(x * xr < 0){
			while(r - l > 1e-4){
				double mid = (l + r )/2;
				double xm = a * mid * mid * mid + b * mid * mid + c * mid + d;
				double xl = a * l * l * l + b * l * l + c * l + d;
				if(xl * xm < 0) r = mid;
				else l = mid;
			}
				printf("%.2lf ", l);
	} 

}
	return 0;
} 

还有一种思路是求出函数的两个极值点x1,x2,那么根一定在这三个小区间[-100, x1], [x1, x2], [x2, 100]中,鉴于我也没有敲,代码就不贴辽~

例2

银行贷款

利率按月累计,我们可以用复利计息法,也就是第一个月还c元后欠银行a(1 + x) - c,a是贷款的原值,x是月利率,c是每月支付的分期付款金额,那么第二个月欠银行(a(1 + x) - c)(1 + x) - c......以此类推,所以我们可以判断在贷款所需的总月数后剩余的钱是否大于等于0,如果是,说明利率过大,否则利率过小。

代码实现:

#include <bits/stdc++.h>

using namespace std;
int m, m1, n;
int main(){
	scanf("%d%d%d", &m, &m1, &n);
	double l = 0, r = 50;  //一般利率都是小于等于1的,但是这道题有数据超过20了!!
	while(r - l > 0.001){
		double mid = (l + r) /2;
		double temp = m;
		for(int i = 1; i <= n; i ++){
			temp = temp * (1 + mid) - m1;
		}
		if(temp >= 0)  r = mid;
		else l = mid;
	}
	printf("%.1f", l * 100);
	return 0;
}

二分答案的重点来了!!!

这类型的题通常会有求...最大值的最小或者求最小值的最大的字眼,而且我们找到的答案区间对题目当中的某个量具有单调性,即在区间中的值越大或越小,题目中的某个量对应增加或减少,我们可以根据这个量来二分区间(相当于从后往前推敲,先枚举答案再判定)。

  1. 先判断题目类型,如果有求...最大值的最小或者求...最小值的最大,那么我们的思路就可以往二分答案这里靠了。
  2. 找出答案所在的区间。
  3. 验证每次二分出来的答案是否可行,在这个过程中,可能会对应题目当中的某个量增加或者减少。
  4. 最后, 我们可以根据答案算出来的这个量和题目当中进行比较,进而二分区间。

先来看求...最大值的最小类题目,对应模板1:

数列分段 Section II

我们可以很明了的看到求每段和最大值的最小,典型的二分答案题目。求每段和最大值的最小,那么答案区间肯定是每段的和所在的区间,再判断我们二分的这个答案能分成几段,如果答案太小,那么分的段数就比题目的多,往右查找。反之,比题目的少,往左查找。

代码实现:

#include <bits/stdc++.h>

using namespace std;
const int N = 1e5 + 10;
int n, m, a[N];
long long sum, tot;

bool check(int mid){
	sum = 0, tot = 0;
	for(int i = 1; i <= n; i ++){
		if(tot + a[i] <= mid) tot += a[i];
		else{
			sum ++;
			tot = a[i];
		}
		
	}
	return sum+1 <= m;  //这里也可以将sum初值赋为1,因为我们是判断和大于mid时,才算前一段结束,这样最后一段就没有计入,所以将sum初值赋为1
}
int main(){
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i ++){
		scanf("%d", &a[i]);
	}
	int max1 = a[1];
	long long sum1 = a[1];
	for(int i = 2; i <= n; i++){
		max1 = max(max1, a[i]);
		sum1 += a[i];
	}
	int l = max1;  //注意这里
    long long r = sum1;
	while(l < r){
		int mid = l + r >>1;
		if(check(mid)) r = mid;  //答案偏大,查找左半区间
		else l = mid + 1;  //答案偏小,查找右半区间
	}
	cout << l << endl;
	return 0;
}

注意这里:为什么我们要把l设为最大值,r设为数列的和呢?[0, 1e9]不可以吗?答案是不可以,按照后一种范围,如果数列中有单个数字比mid大,我们也把它分成了一段,但其实这种做法是错误的,单个数字都比mid大了,那mid肯定不是最大值了呀,所以我们应该把区间设为第一种。

再来看求...最小值的最小类的题,对应模板2

跳石头

最短跳跃距离的最大值,是了,二分答案。距离L是大于等于1的,且最大不会超过起点与终点之间的距离,答案区间也就锁定了。再来看如何判定答案是否可行?我们可以想一下怎样就会移走石头?要使得最短跳跃距离尽可能的长,也就是说当跳跃距离比最短跳跃距离还短,那么我们就移走当前的石头,如果距离太小,那么移走的石头会少一些,就增大距离,往右查找,反之,就减少距离,往左查找。

代码实现:

#include<bits/stdc++.h>
using namespace std;
const int N = 5e4 + 10;
long long a[N], ll, n, m;

bool check(int mid){
	long long last = 0, sum = 0;
	for(int i = 0; i <= n; i ++){
		if((a[i] - last) >= mid){   // 如果当前要跳跃的距离大于等于mid,说明mid是最短距离满足条件,因此判断下一个距离 
			last = a[i];
		}
		
		else
			sum ++;    //如果当前要跳跃的距离小于mid,说明要移走这个石头,因为此时设定mid已经是最短距离,不能再有比它小的距离 
		
	}
	return sum <= m;
}


int main(){
	scanf("%lld%lld%lld", &ll, &n, &m);
	for(int i = 0; i < n; i ++){
		scanf("%lld", &a[i]);
	}
	a[n] = ll;
	long long l = 1, r = ll;
	while(l < r){
		long long mid = l + r + 1>> 1;
		if(check(mid)) {
			l = mid;
		}
		else {
			r = mid - 1;
		}
	}
	printf("%lld", l);
	return 0;
}
	

上面的这种思路是判断移走的石头,其实还有一种思路是判断留下的石头,如果留下的石头数过多,说明移走的石头太少,则距离太短,往右查找,反之,往左查找。

代码实现:

#include<bits/stdc++.h>
using namespace std;
const int N = 5e4 + 10;
long long a[N], ll, n, m;

bool check(int mid){
    int last = 0, sum = 0;
	for(int i = 0; i <= n; i ++){
		if((a[i] - last) >= mid){   
			last = a[i];
			sum ++;
	} 
}
	return sum > n - m ;  //n - m是给定要留下的石头数,sum是依照mid算出来留下的石头数
	
}
int main(){
	scanf("%lld%lld%lld", &ll, &n, &m);
	for(int i = 0; i < n; i ++){
		scanf("%lld", &a[i]);
	}
	a[n] = ll;  //从最后一步到终点的距离也要算
	long long l = 1, r = ll;
	while(l < r){
		long long mid = l + r + 1>> 1;
		if(check(mid)) {
			l = mid;
		}
		else {
			r = mid - 1;
		}
	}
	printf("%lld", l);
	return 0;
}

 这种思路恰好就和进击的奶牛很像,求相邻两头牛之间最近距离的最大值,也就是求牛栏之间最近距离的最大值,如果当前牛栏间距离比mid小,那么应该去掉这个牛栏,增大相邻牛栏间的距离(相当于移走石头),如果比mid大,就计数,因为这里是算留下的牛栏数(相当于留下的石头)。

代码实现:

#include <bits/stdc++.h>

using namespace std;
const int N = 1e5 + 10;
int n, c, a[N];

bool check(int mid){
	int last = a[0], num = 1;   //为什么设为1,因为如果两个牛栏的距离符合条件,那么应该这两个牛栏都留下,设为0相当于只留下了一个 ,也可以最后sum加一
	for(int i = 1; i < n; i ++){
		if(a[i] - last >= mid){
			last = a[i];
			num ++;
		}
			
	}
	if(num >= c)
		return true;
	return false;
}
int main(){
	scanf("%d%d", &n, &c);
	for(int i = 0; i < n; i ++){
		scanf("%d", &a[i]);
	}
	sort(a, a + n);
	int l = 0, r = a[n - 1] - a[0];
	while(l < r){
		int mid = l + r + 1 >> 1;
		if(check(mid)) l = mid ;
		else r = mid - 1;
	}
	cout << l << endl;
	return 0;
}
 

 也可以按照跳石头的第一个思路:

#include <bits/stdc++.h>

using namespace std;
const int N = 1e5 + 10;
int n, c, a[N];

bool check(int mid){
	int last = a[0], num = 0;
	for(int i = 1; i < n; i ++){
		if(a[i] - last >= mid){  //按照跳石头的思路,是判断最短距离为mid时移走的牛栏个数和最多移走牛栏个数的大小 
			last = a[i]; //如果当前的距离大于等于最短距离mid,那么就不移走牛栏,判断下一个相邻距离 
		}
		else
			num++;	//如果当前距离小于mid,应该移走牛栏,因为此时设定mid已经是最短距离,所以当相邻距离小于mid时,应移走当前牛栏 
	}
	return num <= n - c;  //应该是n-c,为什么?因为此时是按照跳石头的思路来写的。题目要求建牛棚的个数是c,也就是说留下的牛栏个数是c,那么移走的牛栏个数也就相当于搬走的石头个数是n-c 
}
int main(){
	scanf("%d%d", &n, &c);
	for(int i = 0; i < n; i ++){
		scanf("%d", &a[i]);
	}
	sort(a, a + n);
	int l = 0, r = a[n - 1] - a[0];
	while(l < r){
		int mid = l + r + 1 >> 1;
		if(check(mid)) l = mid ;
		else r = mid - 1;
	}
	cout << l << endl;
	return 0;
}
 

最后一道!思路都是一样的,按照最小值的最大类题写就可以。

木材加工 - 洛谷

代码实现:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, k, a[N];
long long sum;

bool check(int mid){
	 sum = 0;
	for(int i = 1; i<= n; i ++){
		if(a[i] / mid){
			sum += a[i] / mid;
		}
	}
	return sum >= k;
}
int main(){
	scanf("%d%d", &n, &k);
	for(int i = 1; i<= n; i ++){
		scanf("%d", &a[i]);
	}
	sort(a + 1, a + 1 + n);
	int l = 0, r = a[n];
	while(l < r){
		int mid = l + r + 1 >> 1;
		if(check(mid))  l = mid;
		else r = mid - 1;
	}
	
	cout << l << endl;
	return 0;
}

第一次写了这么多(有些地方也是参考了其他的博客),措辞表达什么的可能有些不是很准确,思路或者代码有不对或者可以改进的地方,欢迎大家指正讨论!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值