author: __Shioko
之前提到的枚舉法是透過考慮所有可能的解來獲得最佳解
而貪心法則是不斷做出看起來最好的選擇直接構造出一組解
也因此貪心演算法的時間複雜度通常會比枚舉還要好
考慮以下問題
我們有無限個面額為{1, 5, 10, 50, 100, 500, 1000}的硬幣
請問最少需要用幾個硬幣才能湊出x元(x是任意正整數)
考慮以下問題
我們有無限個面額為{1, 5, 10, 50, 100, 500, 1000}的硬幣
請問最少需要用幾個硬幣才能湊出x元(x是任意正整數)
通常最直覺的想法就是
不斷使用可以用的面額中最大的那個硬幣
考慮以下問題
我們有無限個面額為{1, 5, 10, 50, 100, 500, 1000}的硬幣
請問最少需要用幾個硬幣才能湊出x元(x是任意正整數)
通常最直覺的想法就是
不斷使用可以用的面額中最大的那個硬幣
例如x = 67時會是50 + 10 + 5 + 1 + 1
最少需要5個硬幣
那如果面額稍微改變一下
這個貪心演算法還會奏效嗎?
那如果面額稍微改變一下
這個貪心演算法還會奏效嗎?
答案是不一定會!
我們可以舉出以下的反例
面額{1, 3, 4}, x = 6
剛才的貪心演算法會得出4 + 1 + 1, 需要3個硬幣
但實際上3 + 3, 兩個硬幣才是最佳解
即使很多時候貪心演算法看起來很直覺
但也很有可能會是錯誤的
因此貪心演算法的證明是非常重要的!
在證明貪心演算法的時候
通常會使用叫做"exchange arguments"的一種證明手法
而這個東西的大致架構如下
第一步: 給你的演算法產出的解和其他的解一個代號
這裡使用A代表貪心法所得出的解, O代表其他任意解
第二步: 比較你的貪心解和其他的解
大致上會有以下兩種作法
如果你的解是要選擇一些元素:
考慮有一個元素不在A裡面 但是在O裡面
以及有一個元素在A裡面 但是不在O裡面
如果你的解是要考慮選擇的順序:
考慮在O當中有某兩個相鄰元素的順序和在A當中的順序不一樣
第三步: 交換!
嘗試去證明, 考慮解O的時候,
把一個不在A當中的元素換成在A當中的元素時,
答案一定不會變得更差,
並且可以在不把答案變差的情況下,
從一個任意的解O不斷的交換一些元素得到解A
因此, 任何最佳解都可以透過這樣交換得到一個一樣好的解A
得證貪心演算法構造出的解A是最佳解
第三步: 交換!
(如果答案是一些選擇的順序)
嘗試去證明, 考慮解O的時候,
有某兩個相鄰元素的順序是和在A的時候不一樣的
而把它們交換回來並不會使答案變差
並且透過一直交換相鄰元素可以得出解A
因此, 任何最佳解都可以透過這樣交換得到一個一樣好的解A
得證貪心演算法構造出的解A是最佳解
總整:
第一步: 給貪心解及其他任意解一個編號
第二步: 比較兩個解不同的地方
第三步: 證明你能把任意解換成你的貪心解, 且不會使答案變差
聽起來可能會有一點抽象
接下來會再討論一些經典的貪心問題
並實際地使用這個證明手法 讓你們更了解整個證明的方式
給定n個活動的開始時間及結束時間
請找到一種排程的方法使得你可以參加盡量多的活動
(不能同時參加兩個以上的活動)
給定n個活動的開始時間及結束時間
請找到一種排程的方法使得你可以參加盡量多的活動
(不能同時參加兩個以上的活動)
範例:
n = 4
活動A : [1, 3]
活動B : [2, 5]
活動C : [3, 9]
活動D : [6, 8]
算法1:
在能挑的活動中從持續時間最短的活動開始挑
反例:
在這個例子中
從最短的活動開始選只能選到一個
但是最佳解是選擇旁邊兩個活動
算法2:
在能挑的活動中從最早開始的活動開始挑
反例:
在這個例子中 選擇先開始的活動明顯不是最佳解
算法3:
在能挑的活動中從最早結束的活動開始挑
雖然聽起來很不直覺
不過這個算法是正確的
至於要怎麼透過exchange argument證明呢?
我們先假設不斷選最早結束活動產出的最佳解是A
證明:
考慮某個最佳解O選了x個活動(依照結束時間排序)
並假設o_1不是第一個結束的活動(a_1)
那我們一定可以把它換成第一個結束的活動
(因為o_1的結束時間比a_1還要晚)
證明:
考慮最佳解O選了x個活動(依照結束時間排序)
同樣地, 假設o_2也不是第二個結束的活動(a_2)
我們也可以用相同的理由把它換成a_2
所以我們可以一直用同樣的手法把某個最佳解O
的元素換掉, 直到O長得和A一模一樣
最後發現我們一定可以把任何最佳解換成算法產出的解A
且答案不會變差
因此, 我們就可以知道算法產出的解A一定是最佳解
最後發現我們一定可以把任何最佳解換成算法產出的解A
且答案不會變差
因此, 我們就可以知道算法產出的解A一定是最佳解
想要自己實作看看的可以丟這題:
CSES - Movie Festival
https://cses.fi/problemset/task/1629
有n個礦工在平面上的y座標軸上,
n個鑽石原礦在x座標軸上
並且每個礦工都要用鉤子挖一個鑽石原礦,
當位在(a, b)的礦工伸出鉤子挖位在(c, d)的鑽石原礦時,
會消耗 的體力
請問若採取最好的策略,
所有礦工消耗的總體力和最少是多少?
範例:
n = 2
礦工位置: (0, 1), (0, -1)
鑽石原礦位置: (-2, 0), (1, 0)
首先先觀察一下
可以發現其實座標軸的正負並不重要
因為當你把礦工從(0, -y)移到(0, y)時
他和所有鑽石原礦的距離都不會改變
同理, 我們也可以把鑽石原礦從(-x, 0)搬到(x, 0)
所以我們就只需要考慮第一象限上的礦工和鑽石原礦了
再做一點觀察
可以發現如果有兩個礦工伸出的鉤子交叉了
則當你交換它們要挖的鑽石原礦之後
總體耗費的體力會減少
證明:
考慮三角不等式可知
OA + OD > AD, OB + OC > BC
因此可推出AB + CD > AD + BC
也就是說, 如果某種解有出現交叉的話,
把交叉的部份變成沒有交叉就可以使答案變小
也就是說, 如果某種解有出現交叉的話,
把交叉的部份變成沒有交叉就可以使答案變小
想到這裡, 你可能就會想出某種"貪心"的作法:
也就是說, 如果某種解有出現交叉的話,
把交叉的部份變成沒有交叉就可以使答案變小
想到這裡, 你可能就會想出某種"貪心"的作法:
"讓所有線段都不產生交叉"
也就是說, 如果某種解有出現交叉的話,
把交叉的部份變成沒有交叉就可以使答案變小
想到這裡, 你可能就會想出某種"貪心"的作法:
"讓所有線段都不產生交叉"
那該如何證明這樣的貪心是對的呢?
證明:
考慮任意一個有交叉出現的解O(不一定是最佳解)
其中o_i代表第i個礦工所挖的鑽石原礦編號
證明:
考慮任意一個有交叉出現的解O(不一定是最佳解)
其中o_i代表第i個礦工所挖的鑽石原礦編號
假設o_i和o_j是其中一個出現交叉的地方
那透過交換o_i和o_j, 我們可以得到一個更佳解
因此有交叉的解O不可能是最佳解
證明:
對於任意有出現交叉的解O, 都不可能是最佳解
因此最佳解不會有任何交叉出現,
而這樣的解只有一種:
x座標第一小的礦工挖y座標第一小的鑽石原礦
x座標第二小的礦工挖y座標第二小的鑽石原礦
...
最後實作的部份
只需要把礦工跟鑽石原礦分別用x座標, y座標排序
並一個一個計算它們消耗的體力即可
總體時間複雜度O(nlgn)
Link: https://codeforces.com/problemset/problem/1495/A
貪心演算法雖然通常很快
但是要想出並證明一個貪心演算法並不是一件簡單的事
想要學好如何使用貪心演算法的話
多證明自己想出來的貪心算法的正確性一定會有幫助的!
CSES - Tower
https://cses.fi/problemset/task/1073
CSES - Tasks And Deadlines
https://cses.fi/problemset/task/1630
CSES - Movie Festival II
https://cses.fi/problemset/task/1632
CodeForces Educational Round 17 pB - USB vs. PS/2
https://codeforces.com/contest/762/problem/B
CodeForces Round #773 (Div.1) pA - Great Sequence
https://codeforces.com/problemset/problem/1641/A
如果覺得以上這些太簡單 一下就秒掉的話
下一頁還有更有挑戰性的題目喔~
AtCoder ARC073 pE - Ball Coloring
https://atcoder.jp/contests/arc073/tasks/arc073_c
CodeForces Round #190 (Div.1) pB - Ciel and Duel
https://codeforces.com/contest/321/problem/B
1. outline for greedy algorithm - exchange arguments
(http://www.cs.cornell.edu/courses/cs482/2007su/exchange.pdf)
2. competitive programmer's hand book
(https://usaco.guide/CPH.pdf#page=67)
USACO guide - greedy algorithm with sorting
https://usaco.guide/silver/greedy-sorting?lang=cpp
AP325 P.108 貪心演算法與掃描線演算法
https://drive.google.com/drive/folders/10hZCMHH0YgsfguVZCHU7EYiG8qJE5f-m