永远不会的二分(二分做不对)

做不对的二分

二分这个知识点,我好像在高中就有在数学课本中接触,到了编程上才知道,二分是真的难。
二分杀我!!!
首先呢,说一说我对做二分题的理解
二分题先要理解题意然后去套模板就可以了,去理解是找>=x的最小值还是<=x的最大值,下面是两个模板。
>=x的最小值模板

while(l < r){
	//mid的类型可以根据具体来变
	int mid = l + r >> 1;
	if(check(mid)) r = mid;
	else l = mid + 1;
}

<=x的最大值模板

while(l < r){
	//mid的类型可以根据具体来变
	int mid = l + r + 1 >> 1;
	if(check(mid)) l = mid;
	else r = mid - 1;
}

以上的两个模板,在我做题的过程中是没有问题的(可能因为我比较菜),不过既然这个模板可以存下来,就证明它是经过很多人试验并补全的。
当然我们不能一味的死套模板,需要根据实际情况加以变通(目前我还不会,没有遇到)

小数二分我认为较为简单,模板如下

while(r - l > eps){//此处的eps需要自己定义精度
	if(check(mid)) l = mid;
	else r = mid;
}

个人认为,无论是整数二分还是小数二分,关键在于用对模板和写好check函数,当然l,r的值也需要注意(后边有例题会专门提到)

首先我们看最经典的跳石头问题

题目
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。

为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。

输入
第一行包含三个整数 L,N,ML,N,M,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证 L≥1 且 N≥M≥0。

接下来 N 行,每行一个整数,第 i 行的整数 Di( 0 < Di <L) 表示第 i 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。

输出
一个整数,即最短跳跃距离的最大值
输入例子
25 5 2
2
11
14
17
21
输出
4
首先我们理解题意,要找跳跃距离的最大值
所以,我们要使用<=x的模板

int l = 0,r = L;//左边界l为0,右边界r为起点石头到终点石头的距离
while(l < r){
	int mid = l + r + 1>> 1;
	if(check(mid)) l = mid;//如果treu就将左边界往右靠
	else r = mid - 1;
}

模板用对后,我们就需要根据题意去写check函数

bool check(int x){
	int now = 0;//now为现在所处石头的坐标,刚开始设为0
	int count = 0;//需要移走的石头数目
	for(int i = 1;i <= n + 1;i++){
		if(a[i] - a[now] < x) count++;/*如果面前的石头到现在所处
石头的距离小于x,我们就需要把面前的石头移走*/
		else now = i;//否则就跳过去然后把现在所处的石头更新
	}
	if(count <= m) return true;//如果需要移走的石头数<=规定的返回true
	else return false;//否则返回false
}

整体代码如下

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 500010; 

int n,m;//n为起点到终点的岩石数,m为组委会  至  多  移走的岩石数。
int L,a[N]; 


bool check(int x){
	int now = 0;
	int count = 0;
	for(int i = 1;i <= n + 1;i++){
		if(a[i] - a[now] < x) count++;
		else now = i;
	}
	if(count <= m) return true;
	else return false;
}

int main(){
	scanf("%d%d%d",&L,&n,&m);
	
	a[0] = 0;a[n + 1] = L;//0为起点石头,n+1为终点石头
	for(int i = 1;i <= n;i ++) scanf("%d",&a[i]);
	
	int l = 0,r = L;
	while(l < r){
		int mid = l + r + 1>> 1;
		if(check(mid)) l = mid;
		else r = mid - 1;
	}
	printf("%d",l);
	return 0;
}

其实按照这个思路来 跳石头还蛮简单的哇

接下来看一个需要对l和r的值特别注意的题。

题目
对于给定的一个长度为N的正整数数列A1∼AN ,现要将其分成 M(M≤N)段,并要求每段连续,且每段和的最大值最小。

关于最大值最小:

例如一数列4 2 4 5 1 要分成 3 段。

将其如下分段:
[4 2][4 5][1]

第一段和为 6,第 2 段和为 9,第 3 段和为 1,和最大值为 9。

将其如下分段:
[4][2 4][5 1]

第一段和为 4,第 2 段和为 6,第 3 段和为 6,和最大值为 6。

并且无论如何分段,最大值不会小于 6。

所以可以得到要将数列4 2 4 5 1 要分成 3 段,每段和的最大值最小为 6。

输入
第 1 行包含两个正整数 N,M。

第 2行包含 N 个空格隔开的非负整数 A_i,含义如题目所述。

输出
一个正整数,即每段和最大值最小为多少。
输入例子
5 3
4 2 4 5 1
输出
6

同样先理解题意,找最大值最小为多少我们就得用>=x的模板

while(l < r){
	int mid = l + r >> 1;
	if(check(mid)) r = mid;
	else l = mid + 1;
} 

但是我们要注意l,r的取值

for(int i = 0;i < n;i ++){
	scanf("%d",&a[i]);
	r += a[i]; //r取了和
	if(a[i] > l) l = a[i];	//l取了最大值
}

我们可以根据题意知道二分的范围,左边界应该不小于数列分段单个最大值,右边界不大于总和。
当l取0的时候是有一个测试点会wa的,我也不知道为啥,根据讨论区大佬所说,如果不取最大的那个的话在判断函数里就要考虑完不成的情况,如果取最大就不用考虑如果不取最大值check函数那里可能要加一些判断条件

整体代码如下(不做过多解释,理解就可)

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1e5 + 10;

int n,m;
int a[N];
int l,r;

bool check(int x){
	int sum = 0,cnt = 0;
	for(int i = 0;i < n;i ++){
		if(sum + a[i] <= x) sum += a[i];
		else sum = a[i],cnt++;
	}
	return cnt < m;
}

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

小数二分就不再过多提及,主要再check函数,可以多加摸索取找到解题思路(不是因为我不熟才不提及的
总之呢,做二分题重点还在理解题意找对模板写好check函数
(以上为个人理解,我还是个编程菜鸟,嘻嘻~)

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 1024 设计师:上身试试 返回首页