貪心演算法 LeetCode 101 學習筆記
LeetCode 101:和你一起你輕鬆刷題(C++)
LeetCode 101: A LeetCode Grinding Guide (C++ Version)
作者:高暢Chang Gao
版本:正式版1.00
學習筆記
1. 貪心演算法
1.1 演算法解釋
貪心演算法或貪心思想就是採用貪心的策略,通過區域性最優從而達到全域性最優。
1.2 分配問題
- 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 上執行,我發現反向迭代器的效率要明顯低於正向迭代器,還不知道是什麼原因,等看完書有所瞭解後再補充。
- 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 區間問題
- 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;
}