使用二分法來解決的問題

Grey Zeng發表於2022-03-07

作者:Grey

原文地址:使用二分法來解決的問題

在一個有序陣列中,找某個數是否存在

OJ見:LeetCode 704. Binary Search

思路:

  1. 先得到中點位置,中點可以把陣列分為左右半邊。
  2. 如果中點位置的值等於目標值,直接返回中點位置。
  3. 如果中點位置的值小於目標值,則去陣列左邊按同樣的方式尋找。
  4. 如果中點位置的值大於目標值,則取陣列右邊按同樣的方式尋找。
  5. 如果最後沒有找到,則返回:-1。

程式碼

class Solution {
    public int search(int[] nums, int target) {
        if(nums == null || nums.length < 1) {
            return -1;
        }
        int l = 0; 
        int r = nums.length - 1;
        while (l <= r) {
            int mid = l + ((r - l)>>1);
            if (target > nums[mid]) {
                l = mid + 1;
            } else if (target == nums[mid]) {
                return mid;
            } else {
                r = mid - 1;
            }
        }
        return -1;
    }
}

在一個有序陣列中,找大於等於某個數最左側的位置

OJ見:牛客-查詢某個位置

這個問題只需要在上例基礎上進行簡單改動即可,上例中,我們找到滿足條件的位置就直接return

if (target == nums[mid]) {
    return mid;
}

在本問題中,因為要找到最左側的位置,所以,在遇到相等的時候,只需要先把位置記錄下來,不用直接返回,然後繼續去左側找是否還有滿足條件的位置。

同時,在遇到target < nums[mid]條件下,也需要記錄下此時的mid位置,因為這也可能是滿足條件的位置。

程式碼:

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

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int k = in.nextInt();
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = in.nextInt();
        }
        // 雖然不排序也可以通過,但是題目未說明一定是有序陣列
        Arrays.sort(arr);
        System.out.println(getNearestLeft(arr, k));
        in.close();
    }

    public static int getNearestLeft(int[] nums, int target) {
        if (nums == null || nums.length < 1) {
            return -1;
        }
        int l = 0;
        int r = nums.length - 1;
        int ans = 0;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if (target < nums[mid]) {
                ans = mid;
                r = mid - 1;
            } else if (target > nums[mid]) {
                l = mid + 1;
            } else {
                ans = mid;
                r = mid - 1;
            }
        }
        return ans;
    }
}

LeetCode上有很多類似的問題,都可以用如上方式解答,比如:

LeetCode 35. Search Insert Position

程式碼見:LeetCode_0035_SearchInsertPosition

LeetCode 34. Find First and Last Position of Element in Sorted Array

程式碼見:LeetCode_0034_FindFirstAndLastPositionOfElementInSortedArray

區域性最大值問題

OJ見:LeetCode 162. Find Peak Element

思路

假設陣列長度為N,首先判斷0號位置的數和N-1位置的數是不是峰值位置。

0號位置只需要和1號位置比較,如果0號位置大,0號位置就是峰值位置。

N-1號位置只需要和N-2號位置比較,如果N-1號位置大,N-1號位置就是峰值位置。

如果0號位置和N-1在上輪比較中均是最小值,那麼陣列的樣子必然是如下情況:

image

[0..1]區間內是增長, [N-2...N-1]區間內是下降

那麼峰值位置必在[1...N-2]之間出現

此時可以通過二分來找峰值位置,先來到中點位置,假設為mid,如果:

arr[mid] > arr[mid+1] && arr[mid] > arr[mid-1]

mid位置即峰值位置

否則,有如下兩種情況:

情況一,趨勢是:

image

則在[1...(mid-1)]區間內繼續上述二分。

情況二,趨勢是:

image

則在[(mid+1)...(N-2)]區間內繼續上述二分。

完整程式碼

public class LeetCode_0162_FindPeakElement {
    public static int findPeakElement(int[] nums) {
        if (nums.length == 1) {
            return 0;
        }
        int l = 0;
        int r = nums.length - 1;
        if (nums[l] > nums[l + 1]) {
            return l;
        }
        if (nums[r] > nums[r - 1]) {
            return r;
        }
        l = l + 1;
        r = r - 1;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if (nums[mid] > nums[mid + 1] && nums[mid] > nums[mid - 1]) {
                return mid;
            }
            if (nums[mid] < nums[mid + 1]) {
                l = mid + 1;
            } else if (nums[mid] < nums[mid - 1]) {
                r = mid - 1;
            }
        }
        return -1;
    }
}

分割陣列的最大值

給定一個非負整數陣列 nums 和一個整數 m ,你需要將這個陣列分成 m 個非空的連續子陣列。設計一個演算法使得這 m 個子陣列各自和的最大值最小。

OJ見:LeetCode 410. Split Array Largest Sum

PS: 此題也可以用四邊形不等式優化的動態規劃來解,但是最優解是二分法

思路

我們先求整個陣列的累加和,假設累加和為sum,我們可以得到一個結論,分割的m個非空連續子陣列的和的範圍一定在(0,sum]區間內。轉換一下思路,如果某種劃分下的子陣列之和的最大值為max,則max首先肯定在(0,sum]區間內。思路轉換為:

子陣列的累加和最大值不能超過max的情況下,最少可分多少部分?

假設能分k個部分,

如果k <= m,說明這種劃分是滿足條件的,我們看max是否可以變的更小。

如果k > m,說明這種劃分是不滿足條件的,我們需要調大max的值。

這裡可以通過二分的方式來定位max的值。即max先取(0,sum]的中點位置,得到的劃分部分k如果k <= m,則max繼續去左邊取中點位置來得到新的劃分k,

如果k > mmax繼續從右邊的中點位置來得到新的劃分k。

完整程式碼

class Solution {
    public static int splitArray(int[] nums, int m) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        int l = 0;
        int r = sum;
        int ans = 0;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            int parts = getParts(nums, mid);
            if (parts > m) {
                // mid越大,parts才會越小
                l = mid + 1;
            } else {
                ans = mid;
                r = mid - 1;
            }
        }
        return ans;
    }

    // 達到aim要分幾部分
    public static int getParts(int[] nums, int aim) {
        for (int num : nums) {
            if (num > aim) {
                return Integer.MAX_VALUE;
            }
        }
        int part = 1;
        int all = nums[0];
        for (int i = 1; i < nums.length; i++) {
            if (all + nums[i] > aim) {
                part++;
                all = nums[i];
            } else {
                all += nums[i];
            }
        }
        return part;
    }
}

其中:int getParts(int[] nums, int aim)方法表示,在不超過aim的情況下,最少需要幾個劃分部分。方法的主要邏輯是:

遍歷陣列,如果發現某個元素的值超過了aim,直接返回系統最大,說明無法得到劃分。如果沒有超過aim,則繼續加入下一個元素,直到超過aim,就定位出一個部分。依次類推,就可以得到最少有幾個劃分。

更多

演算法和資料結構筆記

參考資料

演算法和資料結構體系班-左程雲

相關文章