本文在寫做過程當中參考了大量資料,不能一一列舉,還請見諒。
貪心算法的定義:
貪心算法是指在對問題求解時,老是作出在當前看來是最好的選擇。也就是說,不從總體最優上加以考慮,只作出在某種意義上的局部最優解。貪心算法不是對全部問題都能獲得總體最優解,關鍵是貪心策略的選擇,選擇的貪心策略必須具有無後效性,即某個狀態之前的過程不會影響之後的狀態,只與當前狀態有關。
解題的通常步驟是:
1.創建數學模型來描述問題;
2.把求解的問題分紅若干個子問題;
3.對每一子問題求解,獲得子問題的局部最優解;
4.把子問題的局部最優解合成原來問題的一個解。
若是你們比較瞭解動態規劃,就會發現它們之間的類似之處。最優解問題大部分均可以拆分紅一個個的子問題,把解空間的遍歷視做對子問題樹的遍歷,則以某種形式對樹整個的遍歷一遍就能夠求出最優解,大部分狀況下這是不可行的。貪心算法和動態規劃本質上是對子問題樹的一種修剪,兩種算法要求問題都具備的一個性質就是子問題最優性(組成最優解的每個子問題的解,對於這個子問題自己確定也是最優的)。動態規劃方法表明了這一類問題的通常解法,咱們自底向上構造子問題的解,對每個子樹的根,求出下面每個葉子的值,而且以其中的最優值做爲自身的值,其它的值捨棄。而貪心算法是動態規劃方法的一個特例,能夠證實每個子樹的根的值不取決於下面葉子的值,而只取決於當前問題的情況。換句話說,不須要知道一個節點全部子樹的狀況,就能夠求出這個節點的值。因爲貪心算法的這個特性,它對解空間樹的遍歷不須要自底向上,而只須要自根開始,選擇最優的路,一直走到底就能夠了。
話很少說,咱們來看幾個具體的例子慢慢理解它:
1.活動選擇問題
這是《算法導論》上的例子,也是一個很是經典的問題。有n個須要在同一天使用同一個教室的活動a1,a2,…,an,教室同一時刻只能由一個活動使用。每一個活動ai都有一個開始時間si和結束時間fi 。一旦被選擇後,活動ai就佔據半開時間區間[si,fi)。若是[si,fi]和[sj,fj]互不重疊,ai和aj兩個活動就能夠被安排在這一天。該問題就是要安排這些活動使得儘可能多的活動能不衝突的舉行。例以下圖所示的活動集合S,其中各項活動按照結束時間單調遞增排序。ios
考慮使用貪心算法的解法。爲了方便,咱們用不一樣顏色的線條表明每一個活動,線條的長度就是活動所佔據的時間段,藍色的線條表示咱們已經選擇的活動;紅色的線條表示咱們沒有選擇的活動。
若是咱們每次都選擇開始時間最先的活動,不能獲得最優解:算法
若是咱們每次都選擇持續時間最短的活動,不能獲得最優解:數組
能夠用數學概括法證實,咱們的貪心策略應該是每次選取結束時間最先的活動。直觀上也很好理解,按這種方法選擇相容活動爲未安排活動留下儘量多的時間。這也是把各項活動按照結束時間單調遞增排序的緣由。網絡
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; int N; struct Act { int start; int end; }act[100010]; bool cmp(Act a,Act b) { return a.end<b.end; } int greedy_activity_selector() { int num=1,i=1; for(int j=2;j<=N;j++) { if(act[j].start>=act[i].end) { i=j; num++; } } return num; } int main() { int t; scanf("%d",&t); while(t--) { scanf("%d",&N); for(int i=1;i<=N;i++) { scanf("%lld %lld",&act[i].start,&act[i].end); } act[0].start=-1; act[0].end=-1; sort(act+1,act+N+1,cmp); int res=greedy_activity_selector(); cout<<res<<endl; } }
2.錢幣找零問題
這個問題在咱們的平常生活中就更加廣泛了。假設1元、2元、5元、10元、20元、50元、100元的紙幣分別有c0, c1, c2, c3, c4, c5, c6張。如今要用這些錢來支付K元,至少要用多少張紙幣?用貪心算法的思想,很顯然,每一步儘量用面值大的紙幣便可。在平常生活中咱們天然而然也是這麼作的。在程序中已經事先將Value按照從小到大的順序排好。數據結構
3.再論揹包問題
在從零開始學動態規劃中咱們已經談過三種最基本的揹包問題:零一揹包,部分揹包,徹底揹包。很容易證實,揹包問題不能使用貪心算法。然而咱們考慮這樣一種揹包問題:在選擇物品i裝入揹包時,能夠選擇物品的一部分,而不必定要所有裝入揹包。這時即可以使用貪心算法求解了。計算每種物品的單位重量價值做爲貪心選擇的依據指標,選擇單位重量價值最高的物品,將盡量多的該物品裝入揹包,依此策略一直地進行下去,直到揹包裝滿爲止。在零一揹包問題中貪心選擇之因此不能獲得最優解緣由是貪心選擇沒法保證最終能將揹包裝滿,部分閒置的揹包空間使每公斤揹包空間的價值下降了。在程序中已經事先將單位重量價值按照從大到小的順序排好。學習
#include<iostream> using namespace std; const int N=4; void knapsack(float M,float v[],float w[],float x[]); int main() { float M=50; //揹包所能容納的重量 float w[]={0,10,30,20,5}; //每種物品的重量 float v[]={0,200,400,100,10}; //每種物品的價值 float x[N+1]={0}; //記錄結果的數組 knapsack(M,v,w,x); cout<<"選擇裝下的物品比例:"<<endl; for(int i=1;i<=N;i++) cout<<"["<<i<<"]:"<<x[i]<<endl; } void knapsack(float M,float v[],float w[],float x[]) { int i; //物品整件被裝下 for(i=1;i<=N;i++) { if(w[i]>M) break; x[i]=1; M-=w[i]; } //物品部分被裝下 if(i<=N) x[i]=M/w[i]; }
4.多機調度問題
n個做業組成的做業集,可由m臺相同機器加工處理。要求給出一種做業調度方案,使所給的n個做業在儘量短的時間內由m臺機器加工處理完成。做業不能拆分紅更小的子做業;每一個做業都可在任何一臺機器上加工處理。這個問題是NP徹底問題,尚未有效的解法(求最優解),可是能夠用貪心選擇策略設計出較好的近似算法(求次優解)。當n<=m時,只要將做業時間區間分配給做業便可;當n>m時,首先將n個做業從大到小排序,而後依此順序將做業分配給空閒的處理機。也就是說從剩下的做業中,選擇須要處理時間最長的,而後依次選擇處理時間次長的,直到全部的做業所有處理完畢,或者機器不能再處理其餘做業爲止。若是咱們每次是將須要處理時間最短的做業分配給空閒的機器,那麼可能就會出現其它全部做業都處理完了只剩所需時間最長的做業在處理的狀況,這樣勢必效率較低。在下面的代碼中沒有討論n和m的大小關係,把這兩種狀況合二爲一了。優化
5.小船過河問題
POJ1700是一道經典的貪心算法例題。題目大意是隻有一艘船,能乘2人,船的運行速度爲2人中較慢一人的速度,過去後還需一我的把船劃回來,問把n我的運到對岸,最少須要多久。先將全部人過河所需的時間按照升序排序,咱們考慮把單獨過河所須要時間最多的兩個旅行者送到對岸去,有兩種方式:
1.最快的和次快的過河,而後最快的將船劃回來;次慢的和最慢的過河,而後次快的將船劃回來,所需時間爲:t[0]+2*t[1]+t[n-1];
2.最快的和最慢的過河,而後最快的將船劃回來,最快的和次慢的過河,而後最快的將船劃回來,所需時間爲:2*t[0]+t[n-2]+t[n-1]。
算一下就知道,除此以外的其它狀況用的時間必定更多。每次都運送耗時最長的兩人而不影響其它人,問題具備貪心子結構的性質。
AC代碼:編碼
6.區間覆蓋問題
POJ1328是一道經典的貪心算法例題。題目大意是假設海岸線是一條無限延伸的直線。陸地在海岸線的一側,而海洋在另外一側。每個小的島嶼是海洋上的一個點。雷達坐落於海岸線上,只能覆蓋d距離,因此若是小島可以被覆蓋到的話,它們之間的距離最多爲d。題目要求計算出可以覆蓋給出的全部島嶼的最少雷達數目。對於每一個小島,咱們能夠計算出一個雷達所在位置的區間。spa
問題轉化爲如何用盡量少的點覆蓋這些區間。先將全部區間按照左端點大小排序,初始時須要一個點。若是兩個區間相交而不重合,咱們什麼都不須要作;若是一個區間徹底包含於另一個區間,咱們須要更新區間的右端點;若是兩個區間不相交,咱們須要增長點並更新右端點。
AC代碼:.net
7.銷售比賽
在學校OJ上作的一道比較好的題,這裏碼一下。假設有偶數天,要求天天必須買一件物品或者賣一件物品,只能選擇一種操做而且不能不選,開始手上沒有這種物品。如今給你天天的物品價格表,要求計算最大收益。首先要明白,第一天必須買,最後一天必須賣,而且最後手上沒有物品。那麼除了第一天和最後一天以外咱們每次取兩天,小的買大的賣,而且把賣的價格放進一個最小堆。若是買的價格比堆頂還大,就交換。這樣咱們保證了賣的價格老是大於買的價格,必定能取得最大收益。
下面咱們結合數據結構中的知識講解幾個例子。
8.Huffman編碼
這一樣是《算法導論》上的例子。Huffman編碼是普遍用於數據文件壓縮的十分有效的編碼方法。咱們能夠有多種方式表示文件中的信息,若是用01串表示字符,採用定長編碼表示,則須要3位表示一個字符,整個文件編碼須要300000位;採用變長編碼表示,給頻率高的字符較短的編碼,頻率低的字符較長的編碼,達到總體編碼減小的目的,則整個文件編碼須要(45×1+13×3+12×3+16×3+9×4+5×4)×1000=224000位,因而可知,變長碼比定長碼方案好,總碼長減少約25%。
對每個字符規定一個01串做爲其代碼,並要求任一字符的代碼都不是其餘字符代碼的前綴,這種編碼稱爲前綴碼。可能無前綴碼是一個更好的名字,可是前綴碼是一致承認的標準術語。編碼的前綴性質可使譯碼很是簡單:例如001011101能夠惟一的分解爲0,0,101,1101,於是其譯碼爲aabe。譯碼過程須要方便的取出編碼的前綴,爲此能夠用二叉樹做爲前綴碼的數據結構:樹葉表示給定字符;從樹根到樹葉的路徑看成該字符的前綴碼;代碼中每一位的0或1分別做爲指示某節點到左兒子或右兒子的路標。
從上圖能夠看出,最優前綴編碼碼的二叉樹老是一棵徹底二叉樹,而定長編碼的二叉樹不是一棵徹底二叉樹。 給定編碼字符集C及頻率分佈f,C的一個前綴碼編碼方案對應於一棵二叉樹T。字符c在樹T中的深度記爲dT(c),dT(c)也是字符c的前綴碼長。則平均碼長定義爲:
使平均碼長達到最小的前綴碼編碼方案稱爲C的最優前綴碼。
Huffman編碼的構造方法:先合併最小頻率的2個字符對應的子樹,計算合併後的子樹的頻率;從新排序各個子樹;對上述排序後的子樹序列進行合併;重複上述過程,將所有結點合併成1棵完整的二叉樹;對二叉樹中的邊賦予0、1,獲得各字符的變長編碼。
POJ3253一道就是利用這一思想的典型例題。題目大意是有把一塊無限長的木板鋸成幾塊給定長度的小木板,每次鋸都須要必定費用,費用就是當前鋸的木板的長度。給定各個要求的小木板的長度以及小木板的個數,求最小的費用。以要求3塊長度分別爲5,8,5的木板爲例:先從無限長的木板上鋸下長度爲21的木板,花費21;再從長度爲21的木板上鋸下長度爲5的木板,花費5;再從長度爲16的木板上鋸下長度爲8的木板,花費8;總花費=21+5+8=34。利用Huffman思想,要使總費用最小,那麼每次只選取最小長度的兩塊木板相加,再把這些和累加到總費用中便可。爲了提升效率,使用優先隊列優化,而且還要注意使用long long int保存結果。
AC代碼:
9.Dijkstra算法
Dijkstra算法是由E.W.Dijkstra於1959年提出,是目前公認的最好的求解最短路徑的方法,使用的條件是圖中不能存在負邊。算法解決的是單個源點到其餘頂點的最短路徑問題,其主要特色是每次迭代時選擇的下一個頂點是標記點以外距離源點最近的頂點,簡單的說就是bfs+貪心算法的思想。
10.最小生成樹算法
設一個網絡表示爲無向連通帶權圖G =(V, E) , E中每條邊(v,w)的權爲c[v][w]。若是G的子圖G’是一棵包含G的全部頂點的樹,則稱G’爲G的生成樹。生成樹的代價是指生成樹上各邊權的總和,在G的全部生成樹中,耗費最小的生成樹稱爲G的最小生成樹。例如在設計通訊網絡時,用圖的頂點表示城市,用邊(v,w)的權c[v][w]表示創建城市v和城市w之間的通訊線路所需的費用,最小生成樹給出創建通訊網絡的最經濟方案。
構造最小生成樹的Kruskal算法和Prim算法都利用了MST(最小生成樹)性質:設頂點集U是V的真子集(能夠任意選取),若是(u,v)∈E爲橫跨點集U和V—U的邊,即u∈U,v∈V- U,而且在全部這樣的邊中,(u,v)的權c[u][v]最小,則必定存在G的一棵最小生成樹,它以(u,v)爲其中一條邊。
使用反證法能夠很簡單的證實此性質。假設對G的任意一個最小生成樹T,針對點集U和V—U,(u,v)∈E爲橫跨這2個點集的最小權邊,T不包含該最小權邊<u, v>,但T包括節點u和v。將<u,v>添加到樹T中,樹T將變爲含迴路的子圖,而且該回路上有一條不一樣於<u,v>的邊<u’,v’>,u’∈U,v’∈V-U。將<u’,v’>刪去,獲得另外一個樹T’,即樹T’是經過將T中的邊<u’,v’>替換爲<u,v>獲得的。因爲這2條邊的耗費知足c[u][v]≤c[u’][v’],故即T’耗費≤T的耗費,這與T是任意最小生成樹的假設相矛盾,從而得證。
Prim算法每一步都選擇鏈接U和V-U的權值最小的邊加入生成樹。
Kruskal算法每一步直接將權值最小的不成環的邊加入生成樹,咱們藉助並查集這一數據結構能夠完美實現它。
關於貪心算法的基礎知識就簡要介紹到這裏,但願能做爲你們繼續深刻學習的基礎。