二分搜索 - Binary Search

二分搜索是一种在有序数组中寻找目标值的经典方法,也就是说使用前提是『有序数组』。非常简单的题中『有序』特征非常明显,但更多时候可能需要我们自己去构造『有序数组』。下面我们从最基本的二分搜索开始逐步深入。

一、lower/upper bound

定义 lower bound 为在给定升序数组中大于等于目标值的最小索引,upper bound 则为小于等于目标值的最大索引,下面上代码和测试用例。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        int[] nums = new int[]{1,2,2,3,4,6,6,6,13,18};
        System.out.println(lowerBound(nums, 6)); // 5
        System.out.println(upperBound(nums, 6)); // 7
        System.out.println(lowerBound(nums, 7)); // 8
        System.out.println(upperBound(nums, 7)); // 7
    }

    /*
    * nums[index] >= target, min(index)
    */
    public static int lowerBound(int[] nums, int target) {
        if (nums == null || nums.length == 0) return -1;
        int lb = -1, ub = nums.length;
        while (lb + 1 < ub) {
            int mid = lb + (ub - lb) / 2;
            if (nums[mid] < target) {
                lb = mid;
            } else {
                ub = mid;
            }
        }

        return lb + 1;
    }

    /*
    * nums[index] <= target, max(index)
    */
    public static int upperBound(int[] nums, int target) {
        if (nums == null || nums.length == 0) return -1;
        int lb = -1, ub = nums.length;
        while (lb + 1 < ub) {
            int mid = lb + (ub - lb) / 2;
            if (nums[mid] > target) {
                ub = mid;
            } else {
                lb = mid;
            }
        }

        return ub - 1;
    }
}

源码分析

lowerBound的实现为例,以上二分搜索的模板有几个非常优雅的实现:

  1. while 循环中 lb + 1 < ub, 而不是等号,因为取等号可能会引起死循环。初始化lb < ub 时,最后循环退出时一定有lb + 1 == ub.
  2. mid = lb + (ub - lb) / 2, 可有效防止两数相加后溢出。
  3. lb 和 ub 的初始化,初始化为数组的两端以外,这种初始化方式比起0 和nums.length - 1 有不少优点,详述如下。

如果遇到有问插入索引的位置时,可以分三种典型情况:

  1. 目标值在数组范围之内,最后返回值一定是lb + 1
  2. 目标值比数组最小值还小,此时lb 一直为-1, 故最后返回lb + 1 也没错,也可以将-1 理解为数组前一个更小的值
  3. 目标值大于等于数组最后一个值,由于循环退出条件为  lb + 1 == ub , 那么循环退出时一定有lb = A.length - 1, 应该返回lb + 1

综上所述,返回lb + 1是非常优雅的实现。其实以上三种情况都可以统一为一种方式来理解,即索引-1 对应于数组前方一个非常小的数,索引ub 即对应数组后方一个非常大的数,那么要插入的数就一定在lb 和ub 之间了。

有时复杂的边界条件处理可以通过『补项』这种优雅的方式巧妙处理。

关于lb 和 ub 的初始化,由于mid = lb + (ub - lb) / 2, 且有lb + 1 < ub,故 mid 还是有可能为ub - 1或者lb + 1的,在需要访问mid + 1或者mid - 1处索引的元素时可能会越界。这时候就需要将初始化方式改为lb = 0, ub = A.length - 1 了,最后再加一个关于lb, ub 处索引元素的判断即可。如 Search for a Range 和 Find Peak Element. 尤其是 Find Peak Element 中 lb 和 ub 的初始值如果初始化为-1和数组长度会带来一些麻烦。

二、最优解

除了在有序数组中寻找目标值这种非常直接的二分搜索外,我们还可以利用二分搜索求最优解(最大值/最小值),通常这种题中只是隐含了『有序数组』,需要我们自己构造。

用数学语言来描述就是『求满足某条件 C(x) 的最小/大的 x』,以求最小值为例,对于任意满足条件的 xxx, 如果所有的 x≤x′≤UBx \leq x^\prime \leq UBx≤x′≤UB 对于 C(x′)C(x^\prime)C(x′) 都为真(其中 UB 可能为无穷大,也可能为满足条件的最大的解,如果不满足此条件就不能保证二分搜索的正确性),那么我们就能使用二分搜索进行求解,其中初始化时下界lb 初始化为不满足条件的值LB, 上界初始化为满足条件的上界UB. 随后在while 循环内部每次取中,满足条件就取ub = mid, 否则lb = mid, 那么最后ub 就是要求的最小值。求最大值时类似,只不过处理的是lb.

以 POJ No.1064 为例。

Cable master
Time Limit: 1000MS        Memory Limit: 10000K
Total Submissions: 66707        Accepted: 13737
Description

Inhabitants of the Wonderland have decided to hold a regional programming contest. The Judging Committee has volunteered and has promised to organize the most honest contest ever. It was decided to connect computers for the contestants using a "star" topology - i.e. connect them all to a single central hub. To organize a truly honest contest, the Head of the Judging Committee has decreed to place all contestants evenly around the hub on an equal distance from it.
To buy network cables, the Judging Committee has contacted a local network solutions provider with a request to sell for them a specified number of cables with equal lengths. The Judging Committee wants the cables to be as long as possible to sit contestants as far from each other as possible.
The Cable Master of the company was assigned to the task. He knows the length of each cable in the stock up to a centimeter,and he can cut them with a centimeter precision being told the length of the pieces he must cut. However, this time, the length is not known and the Cable Master is completely puzzled.
You are to help the Cable Master, by writing a program that will determine the maximal possible length of a cable piece that can be cut from the cables in the stock, to get the specified number of pieces.
Input

The first line of the input file contains two integer numb ers N and K, separated by a space. N (1 = N = 10000) is the number of cables in the stock, and K (1 = K = 10000) is the number of requested pieces. The first line is followed by N lines with one number per line, that specify the length of each cable in the stock in meters. All cables are at least 1 meter and at most 100 kilometers in length. All lengths in the input file are written with a centimeter precision, with exactly two digits after a decimal point.
Output

Write to the output file the maximal length (in meters) of the pieces that Cable Master may cut from the cables in the stock to get the requested number of pieces. The number must be written with a centimeter precision, with exactly two digits after a decimal point.
If it is not possible to cut the requested number of pieces each one being at least one centimeter long, then the output file must contain the single number "0.00" (without quotes).
Sample Input

4 11
8.02
7.43
4.57
5.39
Sample Output

2.00

Problem

有 N 条绳子,它们的长度分别为 Li. 如果从它们中切割出 K 条长度相同的绳子的话,这 K 条绳子每条最长能有多长?答案保留到小数点后两位。

输入

N = 4, L = {8.02, 7.43, 4.57, 5.39}, K = 11

输出

2.00

题解

这道题看似是一个最优化问题,我们来尝试下使用模板二的思想求解,**令 C(x)C(x)C(x) 为『可以得到 KKK 条长度为 xxx 的绳子』。**根据题意,我们可以将上述条件进一步细化为:C(x)=∑i(floor(Li/x))≥KC(x) = \sum_i(floor(L_i / x)) \geq KC(x)=i∑(floor(Li/x))≥K

我们现在来分析下可行解的上下界。由于答案保留小数点后两位,显然绳子长度一定大于0,大于0的小数点后保留两位的最小值为0.01, 显然如果问题最后有解,0.01 一定是可行解中最小的,且这个解可以分割出的绳子条数是最多的。一般在 OJ 上不同变量都是会给出范围限制,那么我们将上界初始化为最大范围 + 0.01, 它一定在可行解之外(也可以遍历一遍数组取数组最大值,但其实二分后复杂度相差不大)。使用二分搜索后最后返回lb 即可。

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int k = in.nextInt();
        double[] nums = new double[n];
        for (int i = 0; i < n; i++) {
            nums[i] = in.nextDouble();
        }
        System.out.printf("%.2f\n", Math.floor(solve(nums, k) * 100) / 100);
    }

    public static double solve(double[] nums, int K) {
        double lb = 0.00, ub = 10e5 + 0.01;
        // while (lb + 0.001 < ub) {
      for (int i = 0; i < 100; i++) {
            double mid = lb + (ub - lb) / 2;
            if (C(nums, mid, K)) {
                lb = mid;
            } else {
                ub = mid;
            }
        }
        return lb;
    }

    public static boolean C(double[] nums, double seg, int k) {
        int count = 0;
        for (double num : nums) {
            count += Math.floor(num / seg);
        }
        return count >= k;
    }
}

源码分析

方法C 只做一件事,给定数组nums, 判断是否能切割出K 条长度均为seg 的绳子。while 循环中使用lb + 0.001 < ub, 不能使用0.01, 因为计算mid 时有均值的计算,对于double 型数值否则会有较大误差。

三、二分搜索的 while 结束条件判定

对于整型我们通常使用lb + 1 < ub, 但对于double型数据来说会有些精度上的丢失,使得结束条件不是那么好确定。像上题中采用的方法是题目中使用的精度除10。但有时候这种精度可能还是不够,如果结束条件lb + EPS < ub中使用的 EPS 过小时 double 型数据精度有可能不够从而导致死循环的产生!这时候我们将while循环体替换为for (int i = 0; i < 100; i++), 100 次循环后可以达到 10?3010^{-30}10?30 精度范围,一般都没问题。

原文地址:https://www.cnblogs.com/lyc94620/p/9767087.html

时间: 2024-10-11 22:31:53

二分搜索 - Binary Search的相关文章

《算法导论》习题2.3-5 二分搜索 Binary Search

地球人都知道“二分查找”,方法也非常简单,但是你能不能在10分钟内写出一个没有bug的程序呢? 知易行难,自己动手写一下试一试吧. public class BinarySearch { public static int search(int [] A,int target,int a, int b) { int middle = (a+b)/2; if(a>b) return -1; else if (A[middle]==target) return middle; else if (A[

二分搜索(Binary Search)

当我们在字典中查找某个单的时候,一般我们会翻到一个大致的位置(假设吧,翻到中间位置),开始查找.如果翻到的正好有我们要的词,那运气好,查找结束.如果我们要找的词还在这个位置的前面,那我们对前面的这一半词典继续搜索,翻到某个位置(假设是四分之一的位置)等等.这个二分搜索的工作原理一样.相应的算法就叫做二进制搜索算法. 迭代版本算法: //iterative binary search which returns index of element int binarySearchIterative(

[LeetCode] Find Mode in Binary Search Tree 找二分搜索数的众数

Given a binary search tree (BST) with duplicates, find all the mode(s) (the most frequently occurred element) in the given BST. Assume a BST is defined as follows: The left subtree of a node contains only nodes with keys less than or equal to the nod

Binary Search and Quicksort

1. stdlib/bsearch.c 以下代码实现了有序数组的二分搜索. 1 /* $NetBSD: bsearch.c,v 1.14.6.1 2012/04/17 00:05:25 yamt Exp $ */ 2 3 /* 4 * Copyright (c) 1990, 1993 5 * The Regents of the University of California. All rights reserved. 6 * 7 * Redistribution and use in sou

72【leetcode】经典算法- Lowest Common Ancestor of a Binary Search Tree(lct of bst)

题目描述: 一个二叉搜索树,给定两个节点a,b,求最小的公共祖先 _______6______ / ___2__ ___8__ / \ / 0 _4 7 9 / 3 5 例如: 2,8 -->6 2,4-–>2 原文描述: Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BST. According to the definition of LCA

(双指针、二分Binary Search) leetcode 658. Find K closest Elements

题意:给定一个升序排列的数组,找到k个与x最相近的元素(即差值最小),返回的结果必须要是按升序排好的.如果有两个数与 x的差值一样,优先选择数值较小的那个数. 解法一:双指针(排除法),一个一个删,因为是有序数组,且返回的是连续升序子数组,所以每一次删除的元素一定是位于边界:如果数组含有共 7 个元素,要保留 3 个元素,因此要删除 4 个元素(arr.size()-k):因为要删除的元素都位于边界,于是可以使用双指针(左指针指向数组的第一个元素,右指针指向数组最后一个元素)对撞的方式确定保留区

235. Lowest Common Ancestor of a Binary Search Tree

1. 问题描述 Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BST.According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes v and w as the lowest node in T th

leetcode 109 Convert Sorted List to Binary Search Tree

题目连接 https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree/ Convert Sorted List to Binary Search Tree Description Given a singly linked list where elements are sorted in ascending order, convert it to a height balanced BST. /** * De

Lowest Common Ancestor of a Binary Search Tree

1. Title 235. Lowest Common Ancestor of a Binary Search Tree 2. Http address https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ 3. The question Given a binary search tree (BST), find the lowest common ancestor (LCA) of two