演算法——人的天性貪心演算法
0. 寫在前面
說起貪心演算法,可能是我們最為熟悉的演算法了。正如我標題所說的,貪心演算法之所以稱之為貪心,就是由於它的核心思想和我們人的天性一模一樣。都是選取當前情況下的最優值,但是如何選取一種度量標準,使得我們的貪心能獲得全域性最優,這是一個值得商榷的問題。下面,我們就開始全面的介紹貪心演算法。
1. 貪心演算法的定義
貪心演算法主要使由於這樣一個問題而產生的:
它有N個輸入,而它的解就是由這n個輸入的某個子集組成,但是要滿足某些條件。
這些必須要滿足的條件稱為約束條件。
把滿足約束條件的子集稱為該問題的可行解。
度量可行解優劣的函式稱為目標函式。
目標的取極值的可行解稱為最優解。
以上就是貪心演算法的大概描述,那麼它的具體實現是如何呢,我們下面給出答案。
貪心演算法的步驟:
1. 先選取一種度量標準。
2. 按照這種度量標準對這N個輸入進行排序,然後一個一個輸入。
3. 如果這個輸入不能和當前部分最優解加在一起產生一個可行解,則不把此輸入加入到這部分解中。
沒錯,這就結束了。但是具體的演算法應該是這樣寫的:
//貪心演算法的抽象化控制
procedure GREEDY(A,n)
//A(1:n)包含n個輸入
solution<-Φ //將解向量初始化為空
for i<-1 to n do
x<-SELECT(A)
if FEASIBLE(solution,x)
then solution<-UNION(solution,x)
endif
repeat
return solution
end GREEDY
2. 貪心演算法核心——揹包問題
1.問題描述
之所以把揹包問題單獨列出來進行討論,是因為它的特殊性。由於它是一個經典案例,所以在接下來的很多演算法中,都會看到它的身影。
我們這裡是普通的揹包問題,也就是說可以進行拆分放入的。具體問題描述如下:
已知有N種物品,一個可容納M重量的揹包。每種物品i的重量為
w ,假定講物品i的一部分ixi 放入揹包就會得到pixi 的效益,怎樣才能使裝入物品總重量不超過M,且總收益最大。
2.揹包問題的貪心演算法
1. 問題描述
很顯然這種問題我們經常在故事裡聽到,很顯然,首先我們想到的就是把最貴的東西先裝進去,可是最貴的東西一定是總價格最大的麼?未必,由於總價格最大的潛在因素就有可能它的體積也非常龐大,因此單價不高。我們生活中嫌棄一個東西貴不貴,都是問單價,比如水果啊什麼的,一斤多少錢,這裡也是一樣。那麼我們就可以獲得以下抽象化描述:
目標函式:總收益最大
度量標準:
效益值增量最大
單位效益值最大
2. 演算法描述
有了上述問題分析後,我們就可以有這樣一個演算法:
//揹包問題的貪心演算法
procedure GREEDY-KNAPSACK(P,W,M,X,n)
//P,W分別按照單位效益由高到低排序,M是揹包的容量,X是解向量。
real P(1:n),W(1:n),X(1:n),M,cu
integer i,n;
X←0
cu←M
for i←1 to n do
if W(i)>cu then exit endif
X(i)←1
cu←cu-W(i)
repeat
if i≤n then X(i)←cu/W(i)
endif
end GREEDY-KNAPSACK
對於這個演算法是不是最優演算法,我們可以給出證明,這是最優演算法,但是這裡由於篇幅有限,不在此說明,具體證明可以自行查閱。
3. 貪心演算法若干應用
1. 帶有期限的作業排序
1. 問題描述
有限期作業是這樣的一個問題:有N個作業需要在1臺機器上完成,每個作業均可在單位時間內完成。又假定每個作業i都有一個截止期限
2. 問題求解
通過上述描述,我們可以找到目標函式:總效益最大。而度量標準為:按照收益非增次序排序。這一點和揹包問題相類似。
具體的演算法如下:
//帶有期限和效益的單位時間的作業排序貪心演算法
procedure JS(D,J,n,k)
integer D(0:n),J(0:n),i,k,n,r
D(0)←J(0)←0
k←1;J(1)←1
for i←2 to n do
r←k
while D(J(r))>D(i) and D(J(r))≠r do
r←r-1
repeat
if D(J(r))≤D(i) and D(i)>r then
for i←k to r+1 by -1 do
J(i+1)←J(i)
repeat
J(i+1)←i;k←k+1
endif
repeat
end JS
這個演算法還有優化的空間,大家可以思考一下。
2. 最優歸併模式
1. 問題描述
最優歸併模式問題描述如下:若兩個分別包含n個和m個記錄的已排序檔案可以在O(m+n)時間內歸併到一起而得到一個新排序的檔案。現在給出n個檔案,求最快歸併完成的方案(使用二路歸併)
2. 問題求解
看過這個問題,大家很容易想到哈夫曼樹,沒錯,這個和哈夫曼樹的想法是已知的,每次都歸併尺寸最小的兩個檔案。具體演算法如下:
procedure TREE(L,n)
for i←1 to n-1 do
call GETNODE(T)
LCHILD(T)←LEAST(L) //最小的長度
RCHILD(T)←LEAST(L)
WEIGHT(T)←WEIGHT(LCHILD(T))+WEIGHT(RCHILD(T))
call INSERT(L,T)
repeat
return (LEAST(L))
end TREE
這個程式碼應該很容易看懂,就是把最小的兩個節點合併為新的節點。
3. 最小生成樹
1. 問題描述
最小生成樹是圖論中一個經典的問題,它的解決方法也各不相同,最經典的有兩種,一個是Prim演算法,一個是Kruskal演算法。該問題的具體描述如下:
設G=(V,E)是一個無向連通圖。如果G的生成子圖T(V,E’)是一棵樹,則稱T是G的一棵生成樹。
而生成樹和連通圖的關係是:任何一個具有n個節點的連通圖都必須至少有n-1條邊,而所有具有n-1條邊的n結點連通圖都是樹。
2. Prim演算法
prim演算法的貪心原則是:選擇最短的點加入到解集中。
下面我們不介紹具體程式碼,我們對PRIM演算法進行拆解。我麼您所需的資料結構有:
COST(n,n) 這是成本的鄰接矩陣
T(1:n-1,2)這是解集生成樹
NEAR(n) 目標與解集樹之間的成本最低的結點。
演算法的具體的過程如下:
1. 找到成本最小的邊
2. 初始化NEAR,即找出每個點與已生成樹之間的成本最低的結點。
3. 在對其餘的n-2條邊:
對每一條邊都計算一下已生成樹的成本,並找到最小的一個成本邊。
加入至已生成樹後,對NEAR進行更新。
3. Kruskal演算法
Kruskal演算法與Prim演算法不同,它的貪心原則是:選擇最短的邊加入到解集中。其演算法如下:
procedure Kruskal
T←Φ
while T的邊少於n-1條 do
從E中選取一條最小成本的邊(V,W)
從E中刪去(V,W)
if(V,W)在T中不生成環
then 將(V,W)加到T
else 捨棄(V,W)
endif
repeat
end Kruskal
Kruskal的演算法的難點在於如何判斷一個圖是否為連通圖,一個圖是否存在迴路。這個問題,我們會單獨進行講解。
4. 單點源最短路徑
1. 問題描述
單源點的最短路徑問題也是一個十分經典的演算法問題,這個問題如下:從甲地到乙地有多種路徑,求最短路徑。
2. Dejesk演算法
這種問題尤其是在地圖路徑規劃上能夠用到,它和多段圖的區別在於,它的每個頂點的先後順序是不固定的,如果用多段圖的話,就可以使用動態規劃演算法了,這裡我們使用的Dejesk演算法,它的貪心規則為每次選擇與
關於Dejesk演算法,我們對其核心思想進行講解,不寫具體程式碼,具體程式碼可以在我的Github上找到。
我們所需的資料結構有COST(n,n),這是成本鄰接矩陣,DIST(n),這是從V(出發點)到i點的最短距離,到自己的點為0,S(n)這是解集,裡面只存0(未納入)和1(已納入)。
具體步驟如下:
1. 首先初始化解集S,全置為0,初始化DIST(n)為COST(V,i)
2. 然後把開始結點V納入解集中S(V)←1,DIST(V)←0
3. 對於剩下的n-1條路徑
1)找到DIST(n)中最小的DIST(u)
2) 把u納入到解集中
3)修改DIST(n)中的所有S(i)=0的點,修改方法為DIST(w)←min(DIST(w),DIST(u)+cost(u,w))
這樣,我們的Dejesk演算法就介紹完畢了。這次貪心演算法比較簡單,大家可以自己實現一下。