日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

「二分」有關,算是非常普遍與重要的知識點。然而有很多同學依然沒能很好地掌握,總是會在各種細節上跌跟頭,因此今天我們將對二分算法進行系統地梳理。

「二分」一共有三類常見應用,分別是「整數二分」、「實數二分」以及「二分答案」,接下來將分別介紹這三類應用的具體形式。

一、整數二分

整數二分即為在整數集合上的二分,常見的用法有:在單調序列中查找「某個數是否出現過」、「某個數最早出現的位置」以及「>= 某個數中最小的數」等。

解決這類問題的思想非常統一,即對坐標區間不斷進行折半,以此在 O(log(n)) 的時間復雜度內確定答案,但其「實現方法」卻非常多樣,由此導致很多同學在此類問題上出錯。

因此接下來將通過一個例題來介紹「記錄 ans」的二分實現方法,該方法較易理解且不易出錯。

34. 在排序數組中查找元素的第一個和最后一個位置

題目描述

給定一個按照升序排列的整數數組 nums,和一個目標值 target。找出給定目標值在數組中的開始位置和結束位置,如果數組中不存在目標值 target,返回 [-1, -1]。設計并實現時間復雜度為 O(log(n)) 的算法解決此問題。

示例 1

輸入:nums = [5,7,7,8,8,10], target = 8
輸出:[3,4]

示例 2

輸入:nums = [5,7,7,8,8,10], target = 6
輸出:[-1,-1]

示例 3

輸入:nums = [], target = 0
輸出:[-1,-1]

提示

  • 0 <= nums.length <= 1e5
  • -1e9 <= nums[i] <= 1e9
  • nums 是一個非遞減數組
  • -1e9 <= target <= 1e9

 

題目講解

我們以數組 nums = [3 4 4 4 6], target = 4 進行舉例,首先是確定 4 的開始位置,具體代碼如下:

int l = 0, r = 4, ans = 0;
while (l <= r) {
    int mid = (l + r) / 2;
    if (nums[mid] >= target) {
        ans = mid;
        r = mid - 1;
    }
    else l = mid + 1;
}
if (nums[ans] != target) ans = -1;

上述代碼在二分過程中,[l, r] 始終代表二分搜索的區間,而 ans 則代表「數組中第一個 >= target」的位置。最后再通過判斷 nums[ans] 是否等于 target 來確定 target 是否存在于數組中。

我們來模擬一下這個過程,首先二分搜索區間為 [0, 4],因此 mid = 2。此時 nums[2] >= 4,即 2 可能是「數組中第一個 >= target」的位置,因此更新 ans 為 2。

面試必考的「二分算法」系統梳理

 

又因為 nums[mid] >= 4,即答案一定不在右半區間,即 [3, 4],于是令 r = mid - 1,搜索區間變為 [0, 1]。此時 mid = 0,nums[0] < target,因此 mid 無法更新 ans。

面試必考的「二分算法」系統梳理

 

由于 nums[mid] < target,因此答案一定不在左半區間,于是 l = mid + 1,搜索區間變為 [1, 1]。此時 mid = 1,nums[1] >= target,更新 ans = 1。

面試必考的「二分算法」系統梳理

 

由于 nums[mid] >= target,于是 r = mid - 1,不再滿足 l <= r 的條件,二分過程結束。最終 ans = 1,二分成功。

通過上述這個過程,我們可以總結出「整數二分」的實現規律,首先由 while (l <= r) 來控制二分進程,每次計算 mid = (l + r) / 2,并判斷 mid 是否為可能的答案。如果是,則更新 ans,否則不更新。另外根據 mid 判斷接下來是搜索左半區間 [l, mid - 1],還是右半區間 [mid + 1, r],不斷循環。退出循環后,ans 即為要求的答案。

根據上述示例,大家可以自行完成「查找 target 結束位置」的代碼,并查看下述完整代碼進行驗證。

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int l = 0, r = nums.size() - 1, ans1 = 0, ans2 = 0;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] >= target) {
                ans1 = mid;
                r = mid - 1;
            }
            else l = mid + 1;
        }
        l = 0, r = nums.size() - 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] <= target) {
                ans2 = mid;
                l = mid + 1;
            }
            else r = mid - 1;
        }
        if (nums.size() && nums[ans1] == target) return {ans1, ans2};
        else return {-1, -1};
    }
};

二、實數二分

實數二分即為實數域上的二分,即二分搜索區間 [l, r] 從整數變為了實數。對比整數二分,實數二分的算法思想沒有變化,唯一變化的只有二分實現方法。

通常來說實數二分有兩種實現方法,第一種是通過確定好的精度 eps(如 1e-6、1e-8 等),以 l + eps < r 為循環條件,每次根據在 mid 上的判定,選擇左半區間 [l, mid] 或右半區間 [mid, r]。循環結束后,l 即為最終答案,代碼示例如下:

while (l + 1e-6 < r) {
    double mid = (l + r) / 2;
    if (check(mid)) l = mid;
    else r = mid;
}

有時精度不容易確定,因此直接固定循環次數進行二分,即第二種方法。與第一種方法僅有「循環條件」的區別,代碼示例如下:

for (int i = 0; i < 100; i++) {
    double mid = (l + r) / 2;
    if (check(mid)) l = mid;
    else r = mid;
}

三、二分答案

二分算法成立的關鍵在于單調性,因此任何具有單調性的問題,我們都可以考慮在其上使用二分算法的可能性。正是基于這一點,當問題的答案具有單調性時,我們可以通過二分將「問題的求解」轉化為「問題的判定」,該方法即為「二分答案」。

接下來我們將通過兩道例題進一步地講解該算法。

287. 尋找重復數

題目描述

給定一個包含 n + 1 個整數的數組 nums,其數字都在 1 到 n 之間(包括 1 和 n),可知至少存在一個重復的整數。

假設 nums 只有一個重復的整數,請找出這個重復的數,并只使用 O(1) 的額外空間。

 

示例 1

輸入:nums = [1,3,4,2,2]
輸出:2

示例 2

輸入:nums = [3,1,3,4,2]
輸出:3

示例 3

輸入:nums = [1,1]
輸出:1

示例 4

輸入:nums = [1,1,2]
輸出:1

提示

  • 2 <= n <= 3e4
  • nums.length == n + 1
  • 1 <= nums[i] <= n
  • nums 中只有一個整數出現兩次或多次,其余整數均只出現一次

題目講解

本題有很多種做法,但我們主要以該題為例來講解「二分答案」,因此我們直接進入「二分答案」的思考過程。

一道題想要使用「二分答案」,其最大的前提是「答案有單調性」,而本題所求解的答案是顯然的,即「重復數的值」。所以我們需要找到一個「判定條件」,使得對于二分區間 [l, r],我們可以根據其中值 mid 是否滿足「判定條件」,來確定下一個搜索區間是左半部分還是右半部分。

在本題中,我們令 f(x) 表示數組中 <= x 的個數,則不難發現 f(x) 是關于 x 的單調函數,即隨著 x 的變大,f(x) 要么變大要么不變。

具體來說,假設重復數為 A,則對于所有小于 A 的數 x,f(x) = x;而所有大于等于 A 的數 y,f(y) > y。例如對于數組 [1 2 2 2 3],n = 4,f(1) = 1,f(2) = 4,f(3) = 5,f(4) = 5。

由此我們可以通過「二分答案」來解決本題,對于二分區間 [l, r],其中值為 mid,若 f(mid) > mid,則說明重復數小于等于 mid,因此更新 ans = mid,且將二分區間變為 [l, mid - 1];否則不更新 ans 且二分區間變為 [mid + 1, r]。

二分答案的時間復雜度為 O(log(n)),對于特定的 mid,求解 f(mid) 的時間復雜度為 O(n),因此本題總的時間復雜度為 O(nlog(n)),具體代碼如下所示:

class Solution {
public:
    bool check(vector<int>& nums, int mid) {
        int cnt = 0;
        for (int x:nums) {
            if (x <= mid) cnt++;
        }
        if (cnt > mid) return 1;
        else return 0;
    }
    int findDuplicate(vector<int>& nums) {
        int l = 1, r = nums.size(), ans = 0;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (check(nums, mid)) {
                ans = mid;
                r = mid - 1;
            }
            else l = mid + 1;
        }
        return ans;
    }
};

其中 while 循環中的 check 函數即為「二分答案」問題中的「判定條件」。對于「二分答案」所適用的題目,我們通常只需對 check 函數進行修改,while 循環中的部分基本不變。

 

410. 分割數組的最大值

題目描述

給定一個非負整數數組 nums 和一個整數 m,你需要將這個數組分成 m 個非空的連續子數組。

設計一個算法使得這 m 個子數組各自和的最大值最小。

示例 1

輸入:nums = [7,2,5,10,8], m = 2
輸出:18
解釋:
一共有四種方法將 nums 分割為 2 個子數組。其中最好的方式是將其分為 [7,2,5] 和 [10,8] 。
因為此時這兩個子數組各自的和的最大值為18,在所有情況中最小。

示例 2

輸入:nums = [1,2,3,4,5], m = 2
輸出:9

示例 3

輸入:nums = [1,4,4], m = 3
輸出:4

提示

  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 1e6
  • 1 <= m <= min(50, nums.length)

題目講解

查看題干,「最大值最小」,這是答案具有單調性,可用二分將「問題求解」轉換為「問題判定」最常見、最典型的特征之一。

具體來說,在本題中,若「子數組和的最大值小于等于 x」符合數組分割要求,則比 x 更大的答案也一定符合要求。即當 x 為負數時,數組分割要求無法滿足,但隨著 x 的不斷變大,會有一個分界點 A,當 x 大于等于 A 時,數組分割要求即可滿足,而這個 A 即為本題答案。

由此本題從「求解 A」轉換為了「二分 A 并判斷數組分割要求是否可以滿足」。對于二分區間 [l, r],其中值為 mid,若 mid 滿足數組分割要求,則更新 ans = mid,二分區間變為 [l, mid - 1];否則不更新 ans 且二分區間變為 [mid + 1, r]。

所以本題的「判定條件」為:對于特定的 mid,判斷當子數組和的最大值 <= mid 時,子數組是否可以分成 m 個非空的連續子數組。

對于該「判定條件」,我們可以使用貪心算法,從前往后遍歷數組,用 sum 表示當前分割子數組的和,cnt 表示已經分割出的子數組的數量。若 sum 加上當前值 nums[i] 大于 x,則將 cnt 加 1,并將當前值作為新的一段分割子數組的開頭,即 sum = nums[i]。遍歷結束后,若 cnt <= m,則判定條件可以滿足,否則無法滿足。

由此本題得以解決,具體代碼如下所示:

class Solution {
public:
    bool check(vector<int>& nums, int m, int mid) {
        int len = nums.size(), sum = 0, cnt = 1;
        for (int x:nums) {
            if (x > mid) return 0;
            if (sum + x <= mid) sum += x;
            else sum = x, cnt++;
        }
        return cnt <= m;
    }
    int splitArray(vector<int>& nums, int m) {
        int l = 0, r = 1e9, ans = 0;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (check(nums, m, mid)) {
                ans = mid;
                r = mid - 1;
            }
            else l = mid + 1;
        }
        return ans;
    }
};

總結一下,當問題的答案具有單調性時,我們可以通過「二分答案」將「問題的求解」轉換為「問題的判定」,進而完成解題的過程。

四、綜合練習

最后我們以一道經典二分題為例進行綜合練習,該題目難度較大,也是一道經典的面試題,希望大家能好好理解并掌握。

4. 尋找兩個正序數組的中位數

題目描述

給定兩個大小為 m 和 n 的正序(從小到大)數組 nums1 和 nums2。請你找出并返回這兩個正序數組的中位數,且時間復雜度不高于 O(log (m+n))。

示例 1

輸入:nums1 = [1,3], nums2 = [2]
輸出:2.00000
解釋:合并數組 = [1,2,3] ,中位數 2

示例 2

輸入:nums1 = [1,2], nums2 = [3,4]
輸出:2.50000
解釋:合并數組 = [1,2,3,4] ,中位數 (2 + 3) / 2 = 2.5

示例 3

輸入:nums1 = [0,0], nums2 = [0,0]
輸出:0.00000

示例 4

輸入:nums1 = [], nums2 = [1]
輸出:1.00000

示例 5

輸入:nums1 = [2], nums2 = []
輸出:2.00000

提示

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -1e6 <= nums1[i], nums2[i] <= 1e6

題目講解

首先考慮一下本題能否「二分答案」,由于奇數長度與偶數長度解法類似,因此僅考慮 m + n 為奇數時的情況。

當 m + n 為奇數時,中位數即為兩個數組排序后第 (m + n + 1) / 2 個數,因此令 f(x) 表示兩個數組中 <= x 的數的個數,中位數即為滿足 f(x) >= (m + n + 1) / 2 中最小的 x。由于兩個數組均為正序,因此對于一個特定的 x,我們可以通過兩次二分在 O(log(n) + log(m)) 的時間復雜度內求得 f(x)。

由此我們可以得知「二分答案」的時間復雜度為 O(log(2e6)(log(n)+log(m))),其中 2e6 為「二分答案」初始區間 [-1e6, 1e6] 的長度。因此「二分答案」無法達到「時間復雜度不高于 O(log (m+n))」的要求,我們需要轉換思路。

重新思考中位數的作用,我們可以發現:中位數常用于將一個序列劃分為長度最多相差 1 的兩部分,且其中一部分的元素始終大于等于另一部分。

為了便于表述,將 nums1 和 nums2 數組分別記為 A 和 B。接下來模擬序列的劃分過程,在 A 數組中選取前 i 個數(A[0], A[1], ..., A[i-1]),在 B 數組中選取前 j 個數(B[0], B[1], ..., B[j-1])。這 i + j 個數組成第一部分,剩下的 n + m - i - j 個數為第二部分。

進一步地,當 m + n 為偶數時,兩部分長度一致,且第一部分最大值小于等于第二部分最小值,即:

面試必考的「二分算法」系統梳理

 

此時中位數等于

面試必考的「二分算法」系統梳理

 

;當 m + n 為奇數時,我們規定第一部分長度比第二部分大 1,且兩部分的最值關系依然滿足,即:

面試必考的「二分算法」系統梳理

 


此時中位數等于

面試必考的「二分算法」系統梳理

 

。觀察上述兩個式子,發現無論 m + n 為奇還是偶,均滿足

面試必考的「二分算法」系統梳理

 

。因此為了便于計算,我們將

面試必考的「二分算法」系統梳理

 

面試必考的「二分算法」系統梳理

 

統一為

面試必考的「二分算法」系統梳理

 

(此處為整除)。

又因為 A、B 均為升序數組,因此上述兩個式子可以修改為:

面試必考的「二分算法」系統梳理

 

繼續觀察可以發現,當 i 增大時,j 在減小,即隨著 i 的增加,A[i-1] 不斷增加,而 B[j] 則不斷減小。因此一定存在一個臨界點 (i, j) 滿足 A[i-1] ≤ B[j] ,而 i 再增加 1 則不再滿足,即:

面試必考的「二分算法」系統梳理

 

不難發現,臨界點 (i, j) 剛好滿足 i、j 劃分數組的要求,因此本題可以通過二分 i 的位置,尋找最大的滿足 A[i-1] ≤ B[j] 的 i,即可完成數組劃分并求得中位數。

另外需要注意 i、j 可能會超出數組范圍,因此規定 A[-1] = B[-1] = -1e9,A[m] = B[n] = 1e9。由此本題得以解決,時間復雜度為 O(log(min(m, n))),滿足題目要求。

代碼實現

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        if (nums1.size() > nums2.size()) swap(nums1, nums2);
        int n = nums1.size(), m = nums2.size(), l = 0, r = n, ans = 0;
        while (l <= r) {
            int i = (l + r) / 2, j = (n + m + 1) / 2 - i;
            if (i <= 0 || j >= m || nums1[i-1] <= nums2[j]) {
                ans = i;
                l = i + 1;
            }
            else r = i - 1;
        }
        int i = ans, j = (n + m + 1) / 2 - i, x1 = -1e9, x2 = -1e9, x3 = 1e9, x4 = 1e9;
        if (i > 0) x1 = nums1[i-1];
        if (j > 0) x2 = nums2[j-1];
        if (i < n) x3 = nums1[i];
        if (j < m) x4 = nums2[j];
        if ((n + m) % 2) return max(x1, x2);
        else return (max(x1, x2) + min(x3, x4)) / 2.0;
    }
};

總結

二分是一個非常常見卻又極其精妙的算法,經常成為解題的一個突破口。該算法一共有三類常見應用,即「整數二分」、「實數二分」以及「二分答案」,其中「二分答案」能將「問題的求解」轉換為「問題的判定」,因此應用非常廣泛。

二分并沒有固定的模板與套路,靈活性非常高,因此我們需要深刻理解其算法思想,并加以習題來鞏固,以期早日掌握。

 

本文作者:Gene_Liu

聲明:本文歸 “力扣” 版權所有,如需轉載請聯系。文中部分圖片來源于網絡,如有侵權聯系刪除。

分享到:
標簽:算法
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定