1. 程式人生 > >演算法——人的天性貪心演算法

演算法——人的天性貪心演算法

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,假定講物品i的一部分xi放入揹包就會得到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都有一個截止期限di>0,當且僅當作業i在截止日期前被完成時方可獲得pi>0的效益。

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條 doE中選取一條最小成本的邊(V,W)
        從E中刪去(V,Wif(V,W)T中不生成環
            then 將(V,W)加到T
            else 捨棄(V,Wendif
    repeat
end Kruskal

Kruskal的演算法的難點在於如何判斷一個圖是否為連通圖,一個圖是否存在迴路。這個問題,我們會單獨進行講解。

4. 單點源最短路徑

1. 問題描述

單源點的最短路徑問題也是一個十分經典的演算法問題,這個問題如下:從甲地到乙地有多種路徑,求最短路徑。

2. Dejesk演算法

這種問題尤其是在地圖路徑規劃上能夠用到,它和多段圖的區別在於,它的每個頂點的先後順序是不固定的,如果用多段圖的話,就可以使用動態規劃演算法了,這裡我們使用的Dejesk演算法,它的貪心規則為每次選擇與V0最短的路徑。
關於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演算法就介紹完畢了。這次貪心演算法比較簡單,大家可以自己實現一下。