1. 程式人生 > >不光是查詢值! "二分搜尋"

不光是查詢值! "二分搜尋"

2018-11-14 18:14:15

二分搜尋法,是通過不斷縮小解的可能存在範圍,從而求得問題最優解的方法。在程式設計競賽中,經常會看到二分搜尋法和其他演算法相結合的題目。接下來,給大家介紹幾種經典的二分搜尋法的問題。

一、從有序陣列中查詢某個值

1、lowerBound

問題描述:

給定長度為n的單調不下降數列a和一個數k,求滿足ai >= k條件的最小的i。不存在的情況下輸出n。

限制條件:

1 <= n <= 10 ^ 6

0 <= ai < 10 ^ 9

0 <= k <= 10 ^ 9

問題求解:

如果使用樸素的解法按照順序依次查詢的話,也可以求得答案。但是如果利用數列的有序性這一條件,則可以得到更高效的演算法,也就是採用二分搜尋的方法來進行求解。

這個演算法除了在有序數列查詢值的問題上很有用處外,在求最優解的問題上也非常有用。

讓我們考慮一下“求滿足某個條件C(x)的最小的x”這一問題。對於任意滿足C(x)的x,如果所有的x‘ >= x也滿足C(x')的話,那麼我們就可以使用二分法來求得最小的x。首先我們將左端點設定為不滿足C(x)的值,右端點設定為滿足C(x)的值。然後每次取中點,判斷中點是否滿足並縮小範圍,直到範圍足夠小為止。最後ub就是要求的那個最小值。

最大化的問題也可以使用同樣的方法進行求解。

    // (lb, ub]
    private int lowerBound(int[] nums, int target) {
        int lb = -1;
        int ub = nums.length;
        while (ub - lb > 1) {
            int mid = lb + (ub - lb) / 2;
            if (nums[mid] >= target) ub = mid;
            else lb = mid;
        }
        return ub;
    }

 

2、upperBound

問題描述:

問題求解:

    public int[] searchRange(int[] nums, int target) {
        if (nums == null || nums.length == 0) return new int[]{-1, -1};
        int lb = lowerBound(nums, target);
        int ub = upperBound(nums, target);
        if (lb == nums.length || nums[lb] != target) lb = -1;
        if (ub == 0 || nums[ub - 1] != target) ub = 0;
        return new int[]{lb, ub - 1};
    }

    // (lb, ub]
    private int lowerBound(int[] nums, int target) {
        int lb = -1;
        int ub = nums.length;
        while (ub - lb > 1) {
            int mid = lb + (ub - lb) / 2;
            if (nums[mid] >= target) ub = mid;
            else lb = mid;
        }
        return ub;
    }

    // (lb, ub]
    private int upperBound(int[] nums, int target) {
        int lb = -1;
        int ub = nums.length;
        while (ub - lb > 1) {
            int mid = lb + (ub - lb) / 2;
            if (nums[mid] > target) ub = mid;
            else lb = mid;
        }
        return ub;
    }

 

二、假定一個解並判斷是否可行

Cable master POJ 1064

問題描述:

有N條繩子,它們的長度分別為Li。如果從它們中切割出K條長度相同的繩子的話,這K條繩子每條最長能有多長?答案保留到小數點後2位。

限制條件:

1 <= N <= 10000

1 <= K <= 10000

1 <= Li <= 100000

問題求解:

這個問題可以使用二分搜尋非常容易的解決。讓我們套用二分搜尋的模型試著解決一下這個問題。令:

條件C(x) := 可以得到K條長度為x的繩子

則問題變成了求滿足C(x)條件的最大x。在區間初始話的時候,只需要使用充分大的數INF(> MaxL)作為上界即可:

lb = 0

ub = INF

現在問題變成了如何高效的判定C(x)。由於長度為Li的繩子最多可以切出floor(Li / x)段長度為x的繩子,因此

C(x) = (floor(Li / x)的總和是否大於等於K)

它可以在O(n)的時間內判斷出來。

本題POJ對精度要求很高,因此有兩點需要注意:

1、是需要進行Math.floor(x * 100) / 100,避免四捨五入的問題

2、使用DecimalFormat對輸出的精度進行控制

import java.text.DecimalFormat;
import java.util.Scanner;

public class CableMaster {
    int n;
    int k;
    double[] l;

    private boolean C(double x) {
        long res = 0;
        for (double i : l) res += (int) (i / x);
        return res >= k;
    }

    public void cableMaster() {
        // 求最大值[lb, ub)
        double lb = 0;
        double ub = 100001;

        // 重複迴圈直到解的範圍足夠小
        for (int i = 0; i < 100; i++) {
            double mid = lb + (ub - lb) / 2;
            if (C(mid)) lb = mid;
            else ub = mid;
        }

        DecimalFormat df = new DecimalFormat("0.00");
        lb = Math.floor(lb * 100) / 100;
        System.out.println(df.format(lb));
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        CableMaster cm = new CableMaster();
        while (sc.hasNext()) {
            cm.n = sc.nextInt();
            cm.k = sc.nextInt();
            cm.l = new double[cm.n];
            for (int i = 0; i < cm.n; i++) {
                cm.l[i] = sc.nextDouble();
            }
            cm.cableMaster();
        }
    }
}

 

三、最大化最小值

Aggressive Cows POJ 2456

問題描述:

農夫約翰搭建了一間有N間牛舍的小屋。牛舍排在一條直線上,第i號牛舍在xi的位置。但是他的M頭牛對小屋很不滿意,因此經常互相攻擊。約翰為了防止牛之間互相傷害,因此決定把每頭牛都放在離其他牛儘可能遠的位置。也就是要最大化最近兩頭牛之間的距離。

限制條件:

2 <= N <= 100000

2 <= M <= N

0 <= xi <= 10 ^ 9

問題求解:

類似的最大化最小值或者最小化最大值的問題,通常用二分搜尋法就可以很好的解決。我們定義:

C(d) := 可以安排牛的位置使得最近的兩頭牛的距離不小於d

那麼問題就變成了求滿足C(d)的最大的d。另外最近兩頭距離不小於d也就是所有的牛的間距都大於等於d。

判定C(d)可以使用貪心法進行判斷:

對牛舍位置進行排序;

第一頭牛放在x0牛舍;

如果第i頭牛放到了第xj,那麼第i + 1頭牛就要放入最近的滿足xk - xj >= d的牛舍。

import java.util.Arrays;
import java.util.Scanner;

public class AggressiveCows {
    int n;
    int m;
    int[] x;

    private boolean C(int d) {
        int prevIdx = 0;
        for (int i = 1; i < m; i++) {
            int curIdx = prevIdx + 1;
            while (curIdx < n && x[curIdx] - x[prevIdx] < d) curIdx++;
            if (curIdx == n) return false;
            prevIdx = curIdx;
        }
        return true;
    }

    public int aggressiveCows() {
        Arrays.sort(x);
        int lb = 0;
        int ub = x[n - 1];
        while (ub - lb > 1) {
            int mid = lb + (ub - lb) / 2;
            if (C(mid)) lb = mid;
            else ub = mid;
        }
        return lb;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        AggressiveCows ac = new AggressiveCows();
        while (sc.hasNext()) {
            ac.n = sc.nextInt();
            ac.m = sc.nextInt();
            ac.x = new int[ac.n];
            for (int i = 0; i < ac.n; i++) {
                ac.x[i] = sc.nextInt();
            }
            System.out.println(ac.aggressiveCows());
        }
    }
}

 

四、最大化平均值

問題描述:

有n個物品的重量和價值分別是wi和vi。從中選出k個物品使得單位重量的價值最大。

限制條件:

1 <= k <= n <= 10 ^ 4

1 <= wi, vi <= 10 ^ 6

問題求解:

一般最先想到的方法可能是把物品按照單位重量進行排序,從大到小進行選取。但是這個方法在本題中是不可行的。那麼應該如何求解呢?

實際上,對於本題,使用二分搜尋法可以很好的解決。我們定義

條件C(x) : 可以選擇使得單位重量的價值不小於x

因此原問題就變成了求滿足C(x)的最大的x。那麼應該怎麼判斷C(x)是否可行呢?假設我們選擇了某個物品的集合S,那麼他們的單位重量價值為:

sum(vi) / sum(wi)

因此就變成了判斷是否存在S滿足以下的條件

sum(vi) / sum(wi) >= x

把這個不等式進行變形就可以得到

sum(vi - wi * x) >= 0

因此,就可以進行貪心的選取,對vi - wi * x的值進行排序,貪心的從中選擇k個,看其和是否大於0。由於每次都需要排序,所以判斷的時間複雜度為O(nlogn)。

 

五、Follow Up

  • Search in Rotated Sorted Array

問題描述:

問題求解:

因為沒有重複,所以可以直觀的通過mid和r比較來判斷當前的mid是在前半段還是後半段。

    public int search(int[] nums, int target) {
        if (nums == null || nums.length == 0) return -1;
        int l = 0;
        int r = nums.length - 1;
        // [l, r]
        while (r - l + 1 > 0) {
            int mid = l + (r - l) / 2;
            if (nums[mid] == target) return mid;
            if (nums[mid] > nums[r]) {
                // 這裡的判斷條件是關鍵
                if (nums[mid] > target && target >= nums[l]) r = mid - 1;
                else l = mid + 1;
            }
            else {
                if (target > nums[mid] && target <= nums[r]) l = mid + 1;
                else r = mid - 1;
            }
        }
        return -1;
    }

 

  • Search in Rotated Sorted Array II

問題描述:

問題求解:

帶有重複值的問題就是有可能mid和兩端的值是相等的,在這種情況下就沒有辦法進行有效的判斷了,所以需要對兩端的值進行一下去重操作,然後再使用上述的演算法進行二分查詢。

    public boolean search(int[] nums, int target) {
        if (nums == null || nums.length == 0) return false;
        int l = 0;
        int r = nums.length - 1;
        while (r - l + 1 > 0) {
            while (l < r && nums[l] == nums[l + 1]) l++;
            while (r > l && nums[r] == nums[r - 1]) r--;
            int mid = l + (r - l) / 2;
            if (nums[mid] == target) return true;
            if (nums[mid] > nums[r]) {
                if (target >= nums[l] && target < nums[mid]) r = mid - 1;
                else l = mid + 1;
            }
            else {
                if (target > nums[mid] && target <= nums[r]) l = mid + 1;
                else r = mid - 1;
            }
        }
        return false;
    }