## 定義 對 $Q$ 的每個子問題都做出**當下的最優解**,從而期望此為 $Q$ 的最優解 這樣單看定義可能沒有什麼想法,這邊舉一個找錢的例子 如果一個店員需要找你 $67$ ,他最少需要幾個硬幣(硬幣只有 $1、5、10、50$) 對於找錢的例子來說,最優解就是用當下能給面額最大的硬幣去湊 找 $67$,可以用 $50$ 找一個硬幣,剩下 $17$,已經找了 $1$ 個硬幣 找 $17$,可以用 $10$ 找一個硬幣,剩下 $7$,已經找了 $2$ 個硬幣 找 $7$,可以用 $5$ 找一個硬幣,剩下 $2$,已經找了 $3$ 個硬幣 找 $2$,可以用 $1$ 找兩個硬幣,剩下 $0$,已經找了 $5$ 個硬幣 不過找錢有非常嚴格的定義,不是每個硬幣組合都可以用 Greedy 解 貪婪演算法也算是一種類似**人類的思考方式**,不過很多時候**不能**從大問題開始思考 而是要**一步步**地推進**小問題**,比如下面跑單的例子,你如果用**一整個大時間段**去思考 那就會用暴力的方式去**任意組合直到找到最優**,但如果只考慮前後兩個時間段的關係 那結果就會變成,"要盡量**留時間**去處理下一個事情" 的思考方式 這樣依序推理的方式也就是 Greedy 的其中一種求解方式 "堆答案" ## 選擇問題-外送跑單 這種題型是很經典的,都是像這樣**有時間範圍**的多個選擇,問怎麼選能跑過**最多**選擇 用外送當作舉例,每個單子都是**同樣的價格**,但是**處理時間不同**,問怎麼跑能拿**最多錢** 如果用枚舉(暴力)的方式,只有選跟不選,時間複雜度會達到 $O(2^n)$,但也是最直接的 當然現在的主題是 Greedy,所以就不把枚舉的過程跟做法拉出來說明 先舉個例子方便解析等等的作法,目前有四個單子,時間花費如下 ![image alt](https://i.imgur.com/kqmmcIx.png) 如果用腦袋直接算也知道應該照: 單 $3$、單 $2$、單 $4$ 去跑才能有最多錢 當然大部分人在腦袋中還是用類似枚舉的方式去看,所以這裡先**排序**一下**找規律** ![image alt](https://i.imgur.com/fdRb8i3.png) (這是根據結束時間前後排序的,只是剛好也對到開始時間而已) 一般人把想做更多活動,會做兩種行為 1. 在之後空閒時間排入活動 2. 在當前活動之間排入活動(在**已經確定**的活動中間插入**新的**活動) 但是這種題目**不會**遇到第二種情況,因為大部分都是從**完全沒有活動**開始規劃跑單行程 也就是說,只要確保現在這單跑完後的**空閒時間越多**,接下來能跑得單子應該就**越多** 因為單子時間本身是不變的,在下午 $2$ 點結束跟在早上 $8$ 結束,很明顯**後者剩餘時間多** 用上面的圖例去判斷,如果我在跑完單 $3$ 後選擇單 $1$ (結束時間晚),那就剩下**一個時間段** 但是如果選擇單 $2$ (結束時間早),就會剩**兩個時間段**,也多了一個單 $4$ 可以做後續的選擇 假設單 $4$ 也會跟單 $2$ 重疊到,那還是不影響這個原則,因為選單 $2$ 跟單 $1$ 的答案相同 所以最後可以總結出,找**結束時間越早**的單子就可以跑越多單 剩下就是注意**重疊**或是**題目給的小細節**等等 --- 這裡剛好有一個相似的例題-[e576. 10020 - Minimal Coverage](https://zerojudge.tw/ShowProblem?problemid=e576) ![image alt](https://i.imgur.com/aRDpEoe.png) 給定多組線段坐標$[Li, Ri]$ (在 $X$ 軸上) 請選擇 "最少" 數量的線段,使得選擇的線段完全覆蓋 $[0, M]$ (假設 $M=1$,應選區段 $2$,線段數量為 $1$ ) 題目解析 : 這題需求是完全覆蓋 $[0,M]$ 跟最少數量,很明顯完全覆蓋需要先被考慮 如果有兩種選擇,一個是**多組**小線段,另一個是大線段配小線段 * $[0,1]、[1,2]、[2,3]、...[M-3,M-2]、[M-2,M]$ * $[0,M-2]、[M-1,M+1]$ 很明顯第 $2$ 種選擇並不能滿足需求,並且第 $1$ 種選擇也不是最優解 最優解應該是 $[0,M-2]、[M-2,M]$,只需要 $2$ 個線段就可以覆蓋 $[0,M]$ 進一步把範圍縮小,假如有兩個選擇,$M = 4$ * $[0,1]、[1,2]、[1,3]、[3,4]$ * $[0,3]、[3,4]$ 可能有人看出來了,我舉的例子都是先照**起點**排序,如果起點一樣就照**終點**排序 這樣做的好處是幫助思考,現在這個例子,回到原本 Greedy 的定義: **小問題最優解** 那要處理 $M = 4$,就要先考慮 $M = 1\sim3$,如果現在 $M = 1$,$[0,1]、[0,3]$ 都是最優解 如果現在 $M = 2$,最優解只有 $[0,3]$,$M = 3$ 時也一樣 所以應該可以總結成一個規律,要盡量找**能接合前面線段**並且**結尾越遠越好**的線段 能接合前面線段是為了滿足覆蓋 $[0,M]$ 的條件,結尾越遠越好是 Greedy 的展現 需要注意的是,能接合前面線段的意思有兩種 1. 剛好接合,例如 : $[1,2]、[2,5]$ 2. 部分重疊接合 : $[1,4]、[2,9]$ 因為已經有排序了,所以可以更方便的照順序找到新的線段並接上 最後要注意原本並沒有線段,所以假設一個線段是 $[0,0]$ 方便後面接上 以下是程式的執行順序跟總結 1. 輸入資料跟排序線段 2. 從舊線段去找新線段(要接合舊線段、結尾越遠越好) (註解:階段一) 3. 重複判斷哪個新線段更好(最好的叫 best 新線段) (註解:階段二) 5. 如果目前新線段起點已經超過舊線段結尾,將之前找到的 best 新線段**接到舊線段**上 6. 重複步驟 $2\sim 4$ 直到線段範圍至少覆蓋 $[0,M]$ 7. 在步驟 $2\sim4$ 中如果找不到符合條件的新線段代表無法覆蓋 $[0,M]$ => 直接結束 最後記得同時記錄接上去的線段,因為輸出會用到(我是紀錄線段的 index ) 以下是程式碼跟註解 ```cpp= #include<bits/stdc++.h> using namespace std ; #define P pair<int,int> #define FF first #define SS second P ranges[100005] ; // 題目給定線段 bool cmp1(P a, P b) { // 先比較起點再比較終點 if (a.FF == b.FF) return a.SS < b.SS ; // 由小到大 return a.FF < b.FF ; // 由小到大 } int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) ; int m, t ; cin >> t ; while (t--) { // 共 t 次 vector<int> rans ; // 答案線段index int num = 0, left, right ; ; // 題目給 num 個線段、線段左右數值 cin >> m ; while (cin >> left >> right && (left || right)) { ranges[num] = {left, right} ; num++ ; } sort(ranges, ranges+num) ; // 自定義排序 int idx = 0, max_right = 0, nright, nidx ; // 目前線段最右邊、階段 2 線段最右邊、階段 2 選出線段的index while (max_right < m) { // 階段 1 bool have = false ; nright = 0, nidx = 0 ; while (idx < num) { // 階段 2 if (ranges[idx].FF <= max_right) { // 可連接 have = true ; if (ranges[idx].SS > nright) { // 新線段 nright = ranges[idx].SS ; // 階段 2 最右邊 nidx = idx ; // 紀錄新線段 index } } else // 無符合條件新線段 break ; idx++ ; } if (!have) break ; // 找不到可連接的新線段(無法完全覆蓋) else { rans.push_back(nidx) ; // 紀錄新線段 index max_right = nright ; // 更新目前最右邊 } } if (max_right >= m) { // 完全覆蓋 [0,m] cout << rans.size() << '\n' ; // 答案數量 for (int it:rans) { // 輸出答案線段 cout << ranges[it].FF <<' '<< ranges[it].SS << '\n' ; } } else cout << "0\n" ; // 無法覆蓋 cout << '\n' ; } return 0 ; } ```