二分模板
// 传入的数组必须是升序
// 递归实现二分查找
static int binarySearch(int[] arr, int low, int high, int key){
if(low > high){
// 没找到
return -1;
}
int mid = (high + low) / 2;
int midVal = arr[mid];
if(midVal < key){
return binarySearch(arr, mid+1, high, key);
}else if(midVal > key){
return binarySearch(arr, low, mid-1, key);
}else{
// 找到了
return mid;
}
}
// 迭代实现二分查找
// 注意这个二分查找,找到的是最后一个满足条件的数,而不是第一个!!
static int binarySearch1(int[] arr, int key){
int begin = 0;
int end = arr.length - 1;
int mid;
while(begin <= end){
mid = (begin + end) / 2;
if(arr[mid] == key){
return mid;
}else if(arr[mid] > key){
end = mid - 1;
}else if(arr[mid] < key){
begin = mid + 1;
}
}
// 没找到
return -1;
}
// 迭代实现二分查找:第一个大于等于key的数下标,找不到就返回数组最后一个元素的下一个下标
static int lowerbound(int[] arr, int key){
int begin = 0;
int end = arr.length;
int mid;
while(begin < end){
mid = (begin + end) / 2;
if(arr[mid] >= key){
end = mid;
}else{
begin = mid + 1;
}
}
return begin;
}
// 迭代实现二分查找:第一个大于key的数的下标,找不到就返回数组最后一个元素的下一个下标
// 也就是数组长度
static int upperbound(int[] arr, int key){
int begin = 0;
int end = arr.length;
int mid;
while(begin < end){
mid = (begin + end) / 2;
if(arr[mid] > key){
end = mid;
}else{
begin = mid + 1;
}
}
return begin;
}
1、为防止溢出,mid = (begin + end) / 2可以改写为mid = begin + (end - begin) / 2。
2、为什么lower_bound、upper_bound的end要=n?二分初始区间为[0,n]
考虑到待查询的元素值可能大于数组中的所有元素,此时就可以让它返回n,方便利用二分完成其它操作。
3、lower和upper_bound本质上都是在解决:寻找有序序列中第一个满足某条件的元素的位置,大部分二分问题都能直接归结于这个问题。lower的某条件是:大于等于某个值,upper的某条件是:大于某个值。并且这个条件,在排序后,是先从左到右不满足,然后再满足的!
对于寻找第一个大于等于、第一个大于target的下标,都是返回left(begin)。
常见题型
一定要注意题目中是否给出有序、非降序等词句,如果没有一定要排序!因为有些题目没有排序,但它给你的输入实例是排序的,会被误导。
1、就是直接套公式,不需要任何变形
2、给定值最接近的元素
https://nanti.jisuanke.com/t/T1156
关键在于如何理解:最接近相应给定值的元素值,并且有多个值满足时,要输出最小的一个。
思路:可以用lower_bound找到第一个大于等于给定值的下标,再对这个下标对应的元素值进行判断。
1、如果下标 = 数组长度,说明没有一个满足大于等于条件的元素,那么直接返回最后一个元素。
2、如果下标 = 0,直接返回第一个元素。
3、比较arr[res - 1] 与 arr[res],分别和给定值做差,比较差的绝对值,选用差更小的下标。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scan.nextInt();
}
int m = scan.nextInt();
for (int i = 0; i < m; i++) {
int tmp;
tmp = scan.nextInt();
int res = lowerbound(arr, tmp);
// 主要是判断条件
if(res == 0){
System.out.println(arr[0]);
}else{
if(res == arr.length){
res = res - 1;
}else if(Math.abs(arr[res - 1] - tmp) <= Math.abs(arr[res] - tmp)){
res = res - 1;
}
System.out.println(arr[res]);
}
}
}
static int lowerbound(int[] arr, int tmp){
int begin = 0;
int end = arr.length;
while(begin <= end){
int mid = begin + (end - begin) / 2;
if(arr[mid] >= tmp){
end = mid;
}else{
begin = mid + 1;
}
}
return begin;
}
}
3、等于给定值的元素个数
https://nanti.jisuanke.com/t/T1563
主要需要解决:如何统计等于x的个数,可以利用lower_bound和upper_bound的特性,一个找第一个大于等于的元素,一个找第一个大于的元素,直接利用下标关系相减就可以知道个数,当然还要判断不存在该数的情况。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int n, m;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
m = scan.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scan.nextInt();
}
Arrays.sort(arr);
for (int i = 0; i < m; i++) {
int tmp = scan.nextInt();
int res = lowerbound(arr, tmp);
int res1 = upperbound(arr, tmp);
// 用lowerbound判断元素是否存在
if(res == arr.length || arr[res] != tmp){
System.out.println(0);
}else{
System.out.println(res1 - res);
}
}
}
static int lowerbound(int[] arr, int tmp){
int begin = 0;
int end = arr.length;
int mid;
while(begin < end){
mid = begin + (end - begin) / 2;
if (arr[mid] >= tmp){
end = mid;
}else{
begin = mid + 1;
}
}
return begin;
}
static int upperbound(int[] arr, int tmp){
int begin = 0;
int end= arr.length;
int mid;
while(begin < end){
mid = begin + (end - begin) / 2;
if (arr[mid] > tmp){
end = mid;
}else{
begin = mid + 1;
}
}
return begin;
}
}
4、小于等于给定值的最大值
https://nanti.jisuanke.com/t/T1563
转变思路,小于等于x的最大值,直接求是不好求的,可以利用lower_bound找到的下标进行求解,如果下标对应的值=x,那就直接打印;如果不等于,那下标-1就是答案。当然也别忘了对特殊情况的处理,res=0、res=length。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int n, m;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
m = scan.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scan.nextInt();
}
Arrays.sort(arr);
for (int i = 0; i < m; i++) {
int tmp = scan.nextInt();
int res = binarysearch(arr, tmp);
if(res == arr.length){
System.out.println(arr[res - 1]);
}else{
if(arr[res] == tmp){
System.out.println(arr[res]);
}else{
if(res == 0){
System.out.println(-1);
}else{
System.out.println(arr[res - 1]);
}
}
}
}
}
static int binarysearch(int[] arr, int tmp){
int begin = 0;
int end = arr.length;
int mid;
while(begin < end){
mid = begin + (end - begin) / 2;
if(arr[mid] >= tmp){
end = mid;
}else{
begin = mid + 1;
}
}
return begin;
}
}
5、小于给定值的最大值
https://nanti.jisuanke.com/t/T1556
在4的基础上修改即可。
6、分派(浮点数二分模拟)
https://nanti.jisuanke.com/t/T1157
这类题型就是利用二分查找快速的特点,寻找满足某个方程的解,这个解按题目需求,可能是整数也可能是浮点数,while循环的条件变成了begin和end之间的差值,也可以说是精度EPS。
while(end - begin > EPS){
double mid = begin + (end - begin) / 2;
if(某个条件){
begin = mid;
}else{
end = mid ;
}
}
注意,在用二分进行方程未知数求解时(或模拟求值时),begin和mid的更新都是=mid,while判断条件则是begin和end之间的精度。
本题中,我们把派的体积作为二分对象,begin=0,end=最大派的体积,在while循环中判断当前派的体积是否够分(不要忘了自己),如果够就再进一步进行逼近(派体积最大化),最后结果就是begin。
import java.util.Scanner;
public class Main {
public static int N, F;
public static double[] r = new double[10001];
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
N = scan.nextInt();
F = scan.nextInt();
double begin = 0;
double end = 0;
for (int i = 0; i < N; i++) {
r[i] = scan.nextDouble();
// 找到最大的派大小作为二分右边界
r[i] = Math.PI * r[i] * r[i];
end = Math.max(end, r[i]);
}
double EPS = 0.00001;
while(end - begin > EPS){
double mid = begin + (end - begin) / 2;
if(check(mid) == 1){
// 当前情况下满足划分要求,尝试最大化每个人的派体积
begin = mid;
}else{
end = mid ;
}
}
System.out.printf("%.3f", begin);
}
static int check(double n){
int num = 0;
for (int i = 0; i < N; i++) {
num += r[i] / n;
if(num >= F + 1){
return 1;
}
}
return -1;
}
}
7、计算根号2的近似值
我们可以利用 x * x 函数来求解,第一它是单调递增的(满足二分要求),第二根号2 * 根号2=2,方便判断。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
double begin = 1;
double end = 2;
double eps = 1e-5;
double mid = 0;
while(end - begin > eps){
mid = begin + (end - begin) / 2;
if(f(mid) > 2){
end = mid;
}else{
begin = mid;
}
}
// begin可以最大可能的逼近根号2
System.out.println(begin);
}
static double f(double x){
return x * x;
}
}
7、x的平方根(简单)(整数)
直接模拟,注意使用x / mid == mid,代替mid * mid == x,防止mid * mid溢出。
class Solution {
public int mySqrt(int x) {
if (x == 0 || x == 1) return x;
int left = 0;
int right = x;
int mid;
while (left <= right) {
mid = left + (right - left) / 2;
if (mid == x / mid) {
return mid;
} else if (mid < x / mid) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left - 1;
}
}
7、x的平方根(小数)
如果是小数模拟,就需要精度eps。
class Solution {
public double mySqrt(int x) {
double left = 0;
double right = x;
double mid = 0;
// 模拟精度
double eps = 0.000001;
while (right - left > eps) {
mid = left + (right - left) / 2;
if (mid == x / mid) {
return mid;
} else if (mid < x / mid) {
left = mid;
} else {
right = mid;
}
}
return left;
}
}
8、银行贷款(浮点数方程求解问题)
https://nanti.jisuanke.com/t/T1871
还是用上面的模板,只不过求解的方程变了,注意这种题一般都不会出现值恰好等于的情况,所以只用判断值大于还是小于目标值来逼近方程的解。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int x, y, n;
Scanner scan = new Scanner(System.in);
x = scan.nextInt();
y = scan.nextInt();
n = scan.nextInt();
double begin = 0.00001;
double end = 10;
double eps = 0.00001;
double mid;
while(end - begin > eps){
mid = begin + (end - begin) / 2;
double sum = 0.0;
for (int i = 1; i <= n ; i++) {
sum += y * 1.0 / Math.pow((mid + 1), i);
}
// 值大了要把分母也变大
if (sum > x){
begin = mid;
}else{
end = mid;
}
}
System.out.printf("%.1f", begin * 100);
}
}
9、切绳子(满足给定条件的浮点数二分)
https://nanti.jisuanke.com/t/T1883
用上面同样的模板,判断条件为当前二分的绳子长度能否把n条绳子划分出m条,如果可以我们让begin = mid,最大化可能的绳子长度。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int n, m;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
m = scan.nextInt();
// n条绳子,分出m条长度相同的绳子,这m条最长可以是多长
double[] arr = new double[n];
double begin = 0;
double end = 0;
for (int i = 0; i < n; i++) {
arr[i] = scan.nextDouble();
end = Math.max(arr[i], end);
}
double mid;
double eps = 0.00000001;
while(end - begin > eps){
mid = begin + (end - begin) / 2;
int num = 0;
for (int i = 0; i < n; i++) {
// 当前绳子长度能否分够m根
num += arr[i] / mid;
}
if(num >= m){
// 能够分够,再最大化每根绳子长度
begin = mid;
}else{
// 不能分够,说明绳子长度大了
end = mid;
}
}
System.out.printf("%.6f", begin);
}
}
※10、丢瓶盖(满足给定条件的二分整数查找)
https://nanti.jisuanke.com/t/T1878
这道题跟之前的浮点数模拟不同,因为二分的对象是整数,并且条件是可能等于的,这里使用最普通的二分模板,用ans记录最大的距离值。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int a, b;
Scanner scan = new Scanner(System.in);
a = scan.nextInt();
b = scan.nextInt();
int[] arr = new int[a];
for (int i = 0; i < a; i++) {
arr[i] = scan.nextInt();
}
Arrays.sort(arr);
// 最大距离的最小情况=0
int begin = 0;
// 最大距离的最大情况=末尾-首
int end = arr[a - 1] - arr[0];
int mid = 0;
// 对最近的两个瓶盖的最大距离进行二分
int ans = 0;
while(begin <= end){
mid = begin + (end - begin) / 2;
int cnt = 1;
int tmp = arr[0];
// 看当前二分对象能否满足至少B个瓶盖
for (int i = 1; i < a; i++) {
if((arr[i] - tmp) >= mid){
cnt++;
tmp = arr[i];
}
}
if(cnt >= b){
begin = mid + 1;
ans = mid;
}else{
end = mid - 1;
}
}
System.out.println(ans);
}
}
如果题目是使用二分进行浮点数、整数找满足某条件的值,需要分别使用不同的模板进行求解。
11、和为给定数(定一找一)
https://nanti.jisuanke.com/t/T1158
如果用两个循环遍历,铁定超时,所以我们可以固定一个值,再用二分去找另一半,看能否找到。
注意:因为使用二分前要进行排序,题目只需要输出较小的数组成的数对,只用判断<=m/2即可,避免重复查找。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int n;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
long[] arr = new long[n];
for (int i = 0; i < n; i++) {
arr[i] = scan.nextLong();
}
Arrays.sort(arr);
long m;
m = scan.nextLong();
int res = -1;
for (int i = 0; i < n; i++) {
// 关键,避免重复查找
if(arr[i] <= m / 2){
res = binarysearch(arr, m - arr[i]);
if(res == -1){
continue;
}else{
System.out.printf("%d %d", arr[i], m - arr[i]);
break;
}
}else{
break;
}
}
if(res == -1){
System.out.println("No");
}
}
static int binarysearch(long[] arr, long tmp){
int begin = 0;
int end = arr.length - 1;
int mid;
while(begin <= end){
mid = begin + (end - begin) / 2;
if(arr[mid] == tmp){
return mid;
}else if(arr[mid] > tmp){
end = mid - 1;
}else{
begin = mid + 1;
}
}
return -1;
}
}