貪婪演算法(英語:greedy algorithm),又稱貪心演算法,是一種在每一步選擇中都採取在當前狀態下最好或最佳(即最有利)的選擇,類似於模擬一個「貪心」的人,這個人目光短淺,只看眼前利益,不考慮可能後果,所以可想而知,並不是所有時候都可以得到最優解,使用貪心演算法時,請要確保自己可以「證明」其正確性
在每一個階段選擇當前最佳解,並以此達到整個問題的最佳解。
問題能夠分解成子問題解決,子問題的最優解可以推到最終問題的最優解,而貪婪演算法在最優子結構的問題很有效果。
一般有兩種證明方式,反證法或歸納法。
常見的有兩種
兩者區別在於,第一個是離線的,先處理完再選擇,另一種是在線,邊處理邊選擇。
把當前選擇都當成最優解來看,然後進行比較,如果之後選擇不是最優就反悔,捨棄,如果一樣是最優,就接受,以此類推。
假設能換代幣的幣值有 [1,10,100,1000] , 今天河馬拿了x元去換,則最少能拿到多少個代幣。
因為代幣的數量希望要最少,直覺貪心的想法就是要盡量使用面額大的硬幣,代幣由大
面額的往小的考慮,能換盡量換,剩下不足的再考慮比較小的面額。
程式寫起來很簡單,簡單到如果考試碰到這種題目,會懷疑是不是有陷阱。這個方法很直覺,但怎麼知道它是對的呢? 你可能會說:就很直覺啊,路邊隨便找個小學生也會這樣做。
那麼請看看下面的狀況 : 假設代幣的面額是 [1, 4, 5] 三種,上面的貪心方法會找到正確答案嗎? 假設 x = 8,貪心的方法會先換一個 5 元,然後剩下 3 元,在換 3 個 1 元,所以總共換4個硬幣,不過若是全部換成 4 元的只需要 2 個硬幣。
所以並不是應用在任何場合,必須證明,那如何證明呢?
這邊使用歸納法的方法。
在代幣是 [1, 10, 100, 1000] 的情形下,如果 1 元的代幣總額不小於 10 , 一定可以拿出 10 個換成一個 10 元,所以 1 元代幣的個數不會超過 9 個,同樣的你可以證明 10 元, 100元不會超過9個。也就是說,對任意第 i 種代幣 coin[i],小於 coin[i] 的代幣總額不會超過 coin[i] , 所以 Greedy 是對的。 在[1, 4, 5]的狀況,就沒有這個性質了。
輸入格式:第一行是一個正整數 n ,第二行有 n 個非負整數是對方的戰力,第三行有 n 個非負整數是我方的戰力,同行數字間以空白間格, n 與戰力值不超過 \(10^5\),輸出 : 輸出最多可能的勝場數。
範例輸入:
5
3 1 7 0 2
8 3 5 0 0
輸出
3
我們只要關心可以戰勝的那些場次需要誰去出戰,其他場次贏不了,就在剩下的人裡面隨便挑選去應戰。假設我方目前最弱的戰力是 a 而對方最弱的是 b,如果 a <= b,則 a對誰都不會贏,是沒用的,可以忽略。否則 a > b,則「一定有一個最佳解是派 a 去出戰 b」。 證明如下(反證法):
假設在一個最佳解中,a 對戰 x 而 y 對戰 b,我們將其交換改成「a 對 b 而 y 對x」。根據我們的假設 y >= a , 交換後 a 可以贏 b 而 y 對 x 的戰績不會比 a 對 x差,所以交換後勝場數不會變少。
所以可以將雙方戰力先進行一次排序,然後依序由小往大考慮即可。
當然也可以由大到小考慮,基本原則是能贏的贏一點就好,也可以用優先佇列(PQ)來維護。
在上面的題目中,每個資料都是一個數字,接下來我們來看需要使用struct的情況(或pair),我們來看一個經典題目
員工想用機台,已知每台機台有可以使用的時間(開始時間,結束時間),使用時必須全程在場,也就是不能同時使用兩台機檯,河馬希望能使用越多機台越好,可以賺更多錢,請幫忙計算最多可以參加幾場,注意,使用機台時間必須在開始時間使用否則會被其他員工搶走,以及在使用結束時,當下不可能瞬間使用另一台,也就是前一個機台的結束時間需小於下一個機台的開始時間。
範例輸入
4
1 4
0 3
3 4
4 6
輸出
2
這個題目可以想成,數線上有若干線段,要挑選出最多的不重疊線段。 可能會想,就盡量挑短的,但是看範例就知道,挑短的不是正確答案。所以轉個方向想,若 [x,y] 是所有現存線段中右端點最小的,那麼一定有個最佳解是挑選 [x,y]。
證明: 假設最佳解沒有挑[x,y],令 [a,b] 是最佳解中右端點最小的。 根據我們的假設, y <= b,因此將[x,y] 取代 [a,b] 不會重疊到任何最佳解中的其他線段,所以就得到另外一個最佳解包含[x,y]。
根據此性質,我們可以不斷地將目前最小右端挑選進答案,當然要忽略有重疊的線段,所以只需要依照右端由小排到大,從頭到尾掃描一次就可以得到答案。用struct做排序,要定義一個比較函數,不熟的話記得回去看~
直線上有 N 個要服務的點,每架設一座基地台可以涵蓋直徑 R 範圍以內的服務點。輸入服務點的座標位置以及一個正整數 K ,請問:在架設 K 座基地台以及每個基地台的直徑皆相同的條件下,基地台最小直徑 R 為多少?
(座標範圍不超過 1e9,1≤ K < N ≤ 5e4。)
輸入第一行為N,K
接下來N行為座標
範例輸入:
6 2
5 2 1 7 5 8
範例結果:
3
服務點可以看成數線上的點,基地台可以看成數線上的線段,所以我們要計算以 K 根長度相同的線段蓋住所有的點,求最小的線段長度。
要解決這個問題,先看以下問題:
輸入給數線上 N 個點以及 R,請問最少要用幾根長度 R 的線段才能蓋住所有輸入點。
我們以貪心法則來思考。考慮座標值最小的點 P,如果有一個解的最左端點小於 P,我們可以將此線段右移到左端點對齊 P,這樣的移動不會影響解的合法性,因此,可以得到以下結論:「一定有一個最佳解(最少的 K)是將一根線段的左端放在最小座標點上。」因此,我們可以放上一個線段在 [P,P+R] ,然後略過所有以被蓋住的點,對剩下的點繼續此步驟。重複執行到所有的點被蓋住,就可以找到最小的 K 值。
回到我們的原來問題。假設 \(f(R)\) 是給定長度 R 的最少線段數,那麼上述的演算法可以求得 \(f(R)\)。 所以,當 R 增加時,\(f(R)\)必然只會相同或減少,因為用更長的線段去蓋相同的點,不會需要更多線段。所以我們可以二分搜來找出最小的 R,滿足 \(f(R)\) <= K。
掃描線演算法的名字來自在解某些幾何問題時,想像用一條掃描線沿著某個方向掃描,過程中記錄某些資料或維護某些結構。因此與貪心演算法類似,往往第一步就是排序。常見的題目有數線上的點或線段,以及平面上的點或直線。
輸入數線上的 N 個線段,計算線段聯集的總長度。N 不超過 1e5,座標絕對值皆不超過 1e8
範例輸入:
5
10 20
20 20
30 75
5 15
40 80
範例結果:
65
簡單想法(不好): 在每個線段的左界到右界開個大陣列,標記為1,最後在算出整個陣列中有幾個為1,此方法只適用於小範圍,因為時間複雜度跟空間複雜度都看線段總長度。
想像有跟掃描線,從左掃到右邊,碰到線的左端時,代表要加入新的線段進來,碰到右端,代表一個線段的離開,所以我們只需將線段左端做排序,逐一將線段加入,並且維護好線段的最後一根就好了。
實作的部分我分成了使用struct跟pair,讓大家多熟悉這兩者的用法,並且在sort的部分,struct需要獨立寫一個function,而pair不用。
演算法