一.貪心算法的基本概念 算法
當一個問題具備最優子結構性質時,咱們會想到用動態規劃法去解它。但有時會有更簡單有效的算法。咱們來看一個找硬幣的例子。假設有四種硬幣,它們的面值分別爲二角五分、一角、五分和一分。如今要找給某顧客六角三分錢。這時,咱們會不假思索地拿出2個二角五分的硬幣,1個一角的硬幣和3個一分的硬幣交給顧客。這種找硬幣方法與其餘的找法相比,所拿出的硬幣個數是最少的。這裏,咱們下意識地使用了這樣的找硬幣算法:首先選出一個面值不超過六角三分的最大硬幣,即二角五分;而後從六角三分中減去二角五分,剩下三角八分;再選出一個面值不超過三角八分的最大硬幣,即又一個二角五分,如此一直作下去。這個找硬幣的方法實際上就是貪心算法。顧名思義,貪心算法老是做出在當前看來是最好的選擇。也就是說貪心算法並不從總體最優上加以考慮,它所做出的選擇只是在某種意義上的局部最優選擇。固然,咱們但願貪心算法獲得的最終結果也是總體最優的。上面所說的找硬幣算法獲得的結果就是一個總體最優解。找硬幣問題自己具備最優子結構性質,它能夠用動態規劃算法來解。但咱們看到,用貪心算法更簡單,更直接且解題效率更高。這利用了問題自己的一些特性。例如,上述找硬幣的算法利用了硬幣面值的特殊性。若是硬幣的面值改成一分、五分和一角一分3種,而要找給顧客的是一角五分錢。還用貪心算法,咱們將找給顧客1個一角一分的硬幣和4個一分的硬幣。然而3個五分的硬幣顯然是最好的找法。雖然貪心算法不是對全部問題都能獲得總體最優解,但對範圍至關廣的許多問題它能產生總體最優解。如圖的單源最短路徑問題,最小生成樹問題等。在一些狀況下,即便貪心算法不能獲得總體最優解,但其最終結果倒是最優解的很好的近似解。數組
活動安排問題是能夠用貪心算法有效求解的一個很好的例子。該問題要求高效地安排一系列爭用某一公共資源的活動。貪心算法提供了一個簡單、漂亮的方法使得儘量多的活動能兼容地使用公共資源。優化
設有n個活動的集合e={1,2,…,n},其中每一個活動都要求使用同一資源,如演講會場等,而在同一時間內只有一個活動能使用這一資源。每一個活動i都有一個要求使用該資源的起始時間si和一個結束時間fi,且si<fi。若是選擇了活動i,則它在半開時間區間[si,fi]內佔用資源。若區間[si,fi]與區間[sj,fj]不相交,則稱活動i與活動j是相容的。也就是說,當si≥fi或sj≥fj時,活動i與活動j相容。活動安排問題就是要在所給的活動集合中選出最大的相容活動子集合。spa
在下面所給出的解活動安排問題的貪心算法gpeedyselector中,各活動的起始時間和結束時間存儲於數組s和f{中且按結束時間的非減序:.f1≤f2≤…≤fn排列。若是所給出的活動未按此序排列,咱們能夠用o(nlogn)的時間將它重排。.net
template< class type>code
void greedyselector(int n, type s[ 1, type f[ ], bool a[ ] ]排序
{ a[ 1 ] = true;ci
int j = 1;資源
for (int i=2;i< =n;i+ + ) {
if (s[i]>=f[j]) {
a[i] = true;
j=i;
}
else a[i]= false;
}
}
算法greedyselector中用集合a來存儲所選擇的活動。活動i在集合a中,當且僅當a[i]的值爲true。變量j用以記錄最近一次加入到a中的活動。因爲輸入的活動是按其結束時間的非減序排列的,fj老是當前集合a中全部活動的最大結束時間,即:
貪心算法greedyselector一開始選擇活動1,並將j初始化爲1。而後依次檢查活動i是否與當前已選擇的全部活動相容。若相容則將活動i加人到已選擇活動的集合a中,不然不選擇活動i,而繼續檢查下一活動與集合a中活動的相容性。因爲fi
老是當前集合a中全部活動的最大結束時間,故活動i與當前集合a中全部活動相容的充分且必要的條件是其開始時間s 不早於最近加入集合a中的活動j的結束時間fj,si≥fj。若活動i與之相容,則i成爲最近加人集合a中的活動,於是取代活動j的位置。因爲輸人的活動是以其完成時間的非減序排列的,因此算法greedyselector每次老是選擇具備最先完成時間的相容活動加入集合a中。直觀上按這種方法選擇相容活動就爲未安排活動留下儘量多的時間。也就是說,該算法的貪心選擇的意義是使剩餘的可安排時間段極大化,以便安排儘量多的相容活動。算法greedyselector的效率極高。當輸人的活動已按結束時間的非減序排列,算法只需g(n)的時間來安排n個活動,使最多的活動能相容地使用公共資源。
例:設待安排的11個活動的開始時間和結束時間按結束時間的非減序排列以下:
i |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
s[i] |
1 |
3 |
|
5 |
3 |
5 |
6 |
8 |
8 |
2 |
12 |
f[i] |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
算法greedyselector的計算過程如圖所示。
圖中每行相應於算法的一次迭代。陰影長條表示的活動是已選人集合a中的活動,而空白長條表示的活動是當前正在檢查其相容性的活動。若被檢查的活動i的開始時間si小於最近選擇的活動了的結束時間fj,則不選擇活動i,不然選擇活動i加入集合a中。
貪心算法並不總能求得問題的總體最優解。但對於活動安排問題,貪心算法greedyse—1ector卻總能求得的總體最優解,即它最終所肯定的相容活動集合a的規模最大。咱們能夠用數學概括法來證實這個結論。
事實上,設e={1,2,…,n}爲所給的活動集合。因爲正中活動按結束時間的非減序排列,故活動1具備最先的完成時間。首先咱們要證實活動安排問題有一個最優解以貪心選擇開始,即該最優解中包含活動1。設 是所給的活動安排問題的一個最優解,且a中活動也按結束時間非減序排列,a中的第一個活動是活動k。若k=1,則a就是一個以貪心選擇開始的最優解。若k>1,則咱們設 。因爲f1≤fk,且a中活動是互爲相容的,故b中的活動也是互爲相容的。又因爲b中活動個數與a中活動個數相同,且a是最優的,故b也是最優的。也就是說b是一個以貪心選擇活動1開始的最優活動安排。所以,咱們證實了總存在一個以貪心選擇開始的最優活動安排方案。
進一步,在做了貪心選擇,即選擇了活動1後,原問題就簡化爲對e中全部與活動1相容的活動進行活動安排的子問題。即若a是原問題的一個最優解,則a’=a—{i}是活動安排問題 的一個最優解。事實上,若是咱們能找到e’的一個解b’,它包含比a’更多的活動,則將活動1加入到b’中將產生e的一個解b,它包含比a更多的活動。這與a的最優性矛盾。所以,每一步所做的貪心選擇都將問題簡化爲一個更小的與原問題具備相同形式的子問題。對貪心選擇次數用數學概括法即知,貪心算法greedyselector最終產生原問題的一個最優解。
貪心算法經過一系列的選擇來獲得一個問題的解。它所做的每個選擇都是當前狀態下某種意義的最好選擇,即貪心選擇。但願經過每次所做的貪心選擇致使最終結果是問題的一個最優解。這種啓發式的策略並不總能奏效,然而在許多狀況下確能達到預期的目的。解活動安排問題的貪心算法就是一個例子。下面咱們着重討論能夠用貪心算法求解的問題的通常特徵。
對於一個具體的問題,咱們怎麼知道是否可用貪心算法來解此問題,以及可否獲得問題的一個最優解呢?這個問題很難給予確定的回答。可是,從許多能夠用貪心算法求解的問題中
咱們看到它們通常具備兩個重要的性質:貪心選擇性質和最優子結構性質。
1.貪心選擇性質
所謂貪心選擇性質是指所求問題的總體最優解能夠經過一系列局部最優的選擇,即貪心選擇來達到。這是貪心算法可行的第一個基本要素,也是貪心算法與動態規劃算法的主要區別。在動態規劃算法中,每步所做的選擇每每依賴於相關子問題的解。於是只有在解出相關子問題後,才能做出選擇。而在貪心算法中,僅在當前狀態下做出最好選擇,即局部最優選擇。而後再去解做出這個選擇後產生的相應的子問題。貪心算法所做的貪心選擇能夠依賴於以往所做過的選擇,但決不依賴於未來所做的選擇,也不依賴於子問題的解。正是因爲這種差異,動態規劃算法一般以自底向上的方式解各子問題,而貪心算法則一般以自頂向下的方式進行,以迭代的方式做出相繼的貪心選擇,每做一次貪心選擇就將所求問題簡化爲一個規模更小的子問題。
對於一個具體問題,要肯定它是否具備貪心選擇性質,咱們必須證實每一步所做的貪心選擇最終致使問題的一個總體最優解。一般能夠用咱們在證實活動安排問題的貪心選擇性質時所採用的方法來證實。首先考察問題的一個總體最優解,並證實可修改這個最優解,使其以貪心選擇開始。並且做了貪心選擇後,原問題簡化爲一個規模更小的相似子問題。而後,用數學概括法證實,經過每一步做貪心選擇,最終可獲得問題的一個總體最優解。其中,證實貪心選擇後的問題簡化爲規模更小的相似子問題的關鍵在於利用該問題的最優子結構性質。
2.最優子結構性質
當一個問題的最優解包含着它的子問題的最優解時,稱此問題具備最優子結構性質。問題所具備的這個性質是該問題可用動態規劃算法或貪心算法求解的一個關鍵特徵。在活動安排問題中,其最優子結構性質表現爲:若a是對於正的活動安排問題包含活動1的一個最優解,則相容活動集合a’=a—{1}是對於e’={i∈e:si≥f1}的活動安排問題的一個最優解。
3.貪心算法與動態規劃算法的差別
貪心算法和動態規劃算法都要求問題具備最優子結構性質,這是兩類算法的一個共同點。可是,對於一個具備最優子結構的問題應該選用貪心算法仍是動態規劃算法來求解?是否是能用動態規劃算法求解的問題也能用貪心算法來求解?下面咱們來研究兩個經典的組合優化問題,並以此來講明貪心算法與動態規劃算法的主要差異。
給定n種物品和一個揹包。物品i的重量是w ,其價值爲v ,揹包的容量爲c.問應如何選擇裝入揹包中的物品,使得裝入揹包中物品的總價值最大? 在選擇裝入揹包的物品時,對每種物品i只有兩種選擇,即裝入揹包或不裝入揹包。不能將物品i裝入揹包屢次,也不能只裝入部分的物品i。
此問題的形式化描述是,給定c>0,wi>0,vi>0,1≤i≤n,要求找出一個n元0—1向
量(xl,x2,…,xn), ,使得 ≤c,並且 達到最大。
揹包問題:與0-1揹包問題相似,所不一樣的是在選擇物品i裝入揹包時,能夠選擇物品i的一部分,而不必定要所有裝入揹包。
此問題的形式化描述是,給定c>0,wi>0,vi>0,1≤i≤n,要求找出一個n元向量
(x1,x2,...xn),0≤xi≤1,1≤i≤n 使得 ≤c,並且 達到最大。
這兩類問題都具備最優子結構性質。對於0—1揹包問題,設a是可以裝入容量爲c的揹包的具備最大價值的物品集合,則aj=a-{j}是n-1個物品1,2,…,j—1,j+1,…,n可裝入容量爲c-wi叫的揹包的具備最大價值的物品集合。對於揹包問題,相似地,若它的一個最優解包含物品j,則從該最優解中拿出所含的物品j的那部分重量wi,剩餘的將是n-1個原重物品1,2,…,j-1,j+1,…,n以及重爲wj-wi的物品j中可裝入容量爲c-w的揹包且具備最大價值的物品。
雖然這兩個問題極爲類似,但揹包問題能夠用貪心算法求解,而0·1揹包問題卻不能用貪心算法求解。用貪心算法解揹包問題的基本步驟是,首先計算每種物品單位重量的價值
vj/wi而後,依貪心選擇策略,將盡量多的單位重量價值最高的物品裝入揹包。若將這種物品所有裝入揹包後,揹包內的物品總重量未超過c,則選擇單位重量價值次高的物品並儘量多地裝入揹包。依此策略一直進行下去直到揹包裝滿爲止。具體算法可描述以下:
void knapsack(int n, float m, float v[ ], float w[ ], float x[ ] )
sort(n,v,w);
int i;
for(i= 1;i<= n;i++) x[i] = o;
float c = m;
for (i = 1;i < = n;i ++) {
if (w[i] > c) break;
x[i] = 1;
c-= w[i];
}
if (i < = n) x[i] = c/w[i];
}
算法knapsack的主要計算時間在於將各類物品依其單位重量的價值從大到小排序。所以,算法的計算時間上界爲o(nlogn)。固然,爲了證實算法的正確性,咱們還必須證實揹包問題具備貪心選擇性質。
這種貪心選擇策略對0—1揹包問題就不適用了。看圖2(a)中的例子,揹包的容量爲50千克;物品1重10千克;價值60元;物品2重20千克,價值100元;物品3重30千克;價值120元。所以,物品1每千克價值6元,物品2每千克價值5元,物品3每千克價值4元。若依貪心選擇策略,應首選物品1裝入揹包,然而從圖4—2(b)的各類狀況能夠看出,最優的選擇方案是選擇物品2和物品3裝入揹包。首選物品1的兩種方案都不是最優的。對於揹包問題,貪心選擇最終可獲得最優解,其選擇方案如圖2(c)所示。
對於0—1揹包問題,貪心選擇之因此不能獲得最優解是由於它沒法保證最終能將揹包裝滿,部分揹包空間的閒置使每千克揹包空間所具備的價值下降了。事實上,在考慮0—1揹包問題的物品選擇時,應比較選擇該物品和不選擇該物品所致使的最終結果,而後再做出最好選擇。由此就導出許多互相重疊的於問題。這正是該問題可用動態規劃算法求解的另外一重要特徵。動態規劃算法的確能夠有效地解0—1揹包問題。