1. 程式人生 > 實用技巧 >貪心演算法 LeetCode 101 學習筆記

貪心演算法 LeetCode 101 學習筆記

LeetCode 101:和你一起你輕鬆刷題(C++)
LeetCode 101: A LeetCode Grinding Guide (C++ Version)
作者:高暢Chang Gao
版本:正式版1.00

學習筆記

1. 貪心演算法

1.1 演算法解釋

​ 貪心演算法或貪心思想就是採用貪心的策略,通過區域性最優從而達到全域性最優。

1.2 分配問題

  1. Assign Cookies (Easy)

題目描述:
有一群孩子和一堆餅乾,每個孩子有一個飢餓度,每個餅乾都有一個大小。每個孩子只能吃最多一個餅乾,且只有餅乾的大小大於孩子的飢餓度時,這個孩子才能吃飽。求解最多有多少孩子可以吃飽。

輸入輸出樣例:
輸入兩個陣列,分別代表孩子的飢餓度和餅乾的大小。輸出最多有多少孩子可以吃飽的數量。

Input: [1,2], [1,2,3]
Output: 2

​ 在這個樣例中,可以給兩個孩子喂[1, 2]、[1, 3]、[2, 3] 三種組合中的任意一種。

題解:
因為飢餓度最小的孩子最容易吃飽,所以先考慮這個孩子。為了儘可能使得剩餘的餅乾能夠滿足 更多 飢餓度更大的孩子,所以我們應該把 大於等於這個孩子飢餓度的、且大小最小的餅乾 給這個孩子。滿足這個孩子以後採取同樣的策略來滿足剩下飢餓度最小的孩子,直到所有的孩子都已滿足 或 沒有能夠滿足條件的餅乾為止(在下面程式碼中 <==> 孩子全部遍歷 && 餅乾全部遍歷)。

​ 簡而言之,這裡的貪心策略:給剩餘孩子裡飢餓度最小的孩子分配最小的能飽腹的餅乾。

​ 至於具體實現,首先需要給孩子和餅乾分別排序,這樣就可以從飢餓度最小的孩子和最小的餅乾出發,計算有多少個對子 能滿足條件。

int findContentdChildren(vector<int>& children, vector<int>& cookies) {
    sort(children.begin(), children.end());
    sort(cookies.begin(), cookies.end());
    int childPos=0;
    int cookiePos=0;
    while(childPos < children.size() && cookiePos < cookies.size()) {
        if(children[childPos] <= cookies[cookiePos])
            ++childPos;
        ++cookiePos;
    }
    return childPos;
}

拓展:也可以反向考慮,每次使得餅乾能夠滿足所能 飽腹的 飢餓度最大的孩子。

int findContentdChildren(vector<int>& children, vector<int>& cookies) {
    sort(children.rbegin(), children.rend());
    sort(cookies.rbegin(), cookies.rend());
    int childPos=0;
    int cookiePos=0;
    while(childPos < children.size() && cookiePos < cookies.size()) {
        if(cookies[cookiePos] >= children[childPos])
            ++cookiePos;
        ++childPos;
    }
    return cookiePos;
}

這裡比較兩個演算法執行效率:

  • 第一個演算法多數執行時間在 75~90 之間,記憶體消耗在 17.3~17.5 之間
  • 第二個演算法多數執行時間在 124~140 之間,記憶體消耗在 17.4~17.7 之間

通過在 leetcode 上執行,我發現反向迭代器的效率要明顯低於正向迭代器,還不知道是什麼原因,等看完書有所瞭解後再補充。

  1. Candy (Hard)

題目描述:
一群孩子站成一排,每一個孩子有自己的評分。現在需要給這些孩子發糖果,規則是如果一個孩子的評分比自己身旁的一個孩子要高,那麼這個孩子就必須得到比身旁孩子更多的糖果;所有孩子至少要有一個糖果。求解最少需要多少個糖果。

輸入輸出樣例:
輸入是一個數組,表示孩子的評分。輸出是最少糖果的數量。

 Input: [1,0,2]
 Output: 5

在這個樣例中,最少的糖果分法是[2,1,2]。

題解:
對於這道題,我們只需兩次遍歷即可:題目中說“所有孩子至少要有一個糖果” ,那麼我們首先把所有孩子的糖果初始化為1;先從左往右遍歷一次,如果右邊孩子的評分比左邊孩子評分高,則右邊孩子的糖果數更新為左邊孩子糖果數加1;再從右往左遍歷一次,如果左邊孩子的評分比右邊孩子評分高,且左邊孩子的糖果數小於等於右邊孩子糖果數,則左邊孩子的糖果數更新為右邊孩子糖果數加1。

​ 通過兩次遍歷,給每個孩子分配的糖果數就可以滿足題目的要求了。這裡的貪心策略即為,在每次遍歷中,只考慮並更新相鄰一側的大小關係。

int candy(vector<int>& ratings) {
    if(ratings.size() < 2) {
        return ratings.size();
    }
    vector<int> num(ratings.size(), 1);
    for(int i=1; i<ratings.size(); ++i) {
        if(ratings[i] > ratings[i-1]) {
            num[i] = num[i-1] + 1;
        }
    }
    for(int i=ratings.size() - 2; i>=0; --i) {
        if(ratings[i] > ratings[i+1]) {
            num[i] = max(num[i], num[i+1] + 1);
        }
    }
    return accumulate(num.begin(), num.end(), 0); //std::accumulate 可以很方便的求和
}

對於從右往左的遍歷:再從右往左遍歷一次,如果左邊孩子的評分比右邊孩子評分高,且左邊孩子的糖果數小於等於右邊孩子糖果數,則左邊孩子的糖果數更新為右邊孩子糖果數加1。,if語句是否可以改一下

int candy(vector<int>& ratings) {
    if(ratings.size() < 2) {
        return ratings.size();
    }
    vector<int> num(ratings.size(), 1);
    for(int i=1; i<ratings.size(); ++i) {
        if(ratings[i] > ratings[i-1]) {
            num[i] = num[i-1] + 1;
        }
    }
    for(int i=ratings.size() - 2; i>=0; --i) {
        if(ratings[i] > ratings[i+1] && num[i] <= num[i+1]) {
            num[i] = num[i+1] + 1;
        }
    }
    return accumulate(num.begin(), num.end(), 0); //std::accumulate 可以很方便的求和
}

這裡比較兩個演算法執行效率:

  • 第一個演算法多數執行時間在 40~56 之間,記憶體消耗在 16.9~17.0 之間
  • 第二個演算法多數執行時間在 36~40 之間,記憶體消耗在 16.8~17.0 之間

通過在leetcode上執行,修改後的if語句效率略高於之前的if。

1.3 區間問題

  1. Non-overlapping Intervals (Medium)

題目描述:
給定多個區間,計算讓這些區間互不重疊所需要移除區間的最少個數。起止相連不算重疊。

輸入輸出樣例:
輸入是一個數組,陣列由多個長度固定為2 的陣列組成,表示區間的開始和結尾。輸出一個整數,表示需要移除的區間數量。

Input: [[1,2], [2,4], [1,3]]
Output: 1

在這個樣例中,我們可以移除區間[1,3],使得剩餘的區間[[1,2], [2,4]] 互不重疊。

題解:

​ 在選擇要保留的區間時,區間的結尾很重要:選擇的區間結尾越小,餘留給其他區間的空間就越大,也就越能保留更多的區間。因此,我們的貪心策略為,優先保留區間結尾小且不重疊的區間。

​ 具體的方法:先把區間按照結尾的大小進行增序排列(用std::sort()函式進行自定義排序),然後選擇結尾最小且與前一個已保留的區間 不重疊的區間與下一個區間進行判斷,判斷是否保留下一個區間,若不保留,則需要移除的數量加1;否則,下一個區間保留,並用該區間以同樣的方式進行比較。

注意:需要根據實際情況判斷按 區間開頭排序 還是按 區間結尾排序。

bool compare(vector<int>& a, vector<int>& b) {
    return a[1] < b[1];
}

int eraseOverlapIntervals(vector<vector<int>>& intervals) {
    if(intervals.empty()) {
        return 0;
    }
    
    int num=intervals.size();
    sort(intervals.begin(), intervals.end(), compare);
    int total = 0;
    int prev = intervals[0][1];
    
    for(int i=1; i < num; ++i) {
        if(prev > intervals[i][0]) {
            ++total;
        } else {
            prev = intervals[i][1];
        }
    }
    
    return total;
}