五大常用算法之贪心算法
注:因本文撰寫(xiě)的時(shí)候參考了大量資料和博文,出處在此就不全部列出了,非常感謝前輩們分享的學(xué)習(xí)心得
有人歸納了計(jì)算機(jī)的五大常用算法,它們是貪心算法,動(dòng)態(tài)規(guī)劃算法,分治算法,回溯算法以及分支限界算法。雖然不知道為何要將這五個(gè)算法歸為最常用的算法,但是毫無(wú)疑問(wèn),這五個(gè)算法是有很多應(yīng)用場(chǎng)景的,最優(yōu)化問(wèn)題大多可以利用這些算法解決 ,接下來(lái)介紹的就是貪心算法。
一 貪心算法的概念?
所謂貪心算法,是指在對(duì)問(wèn)題求解時(shí),總是做出在當(dāng)前看來(lái)是最好的選擇。也就是說(shuō),不從整體最優(yōu)上加以考慮,他所做出的僅是在某種意義上的局部最優(yōu)解。舉個(gè)例子就很清楚了:現(xiàn)在有一個(gè)能裝4斤蘋(píng)果的袋子,蘋(píng)果有兩種,一種3斤一個(gè),一種2斤一個(gè),怎么裝才能得到最多蘋(píng)果?當(dāng)然如果是我們?nèi)丝紤]的話(huà)當(dāng)然是拿兩個(gè)2斤的蘋(píng)果,就剛好裝滿(mǎn)了,但是如果按貪心算法拿的話(huà),首先就要把最重的那個(gè)3斤的蘋(píng)果拿下(局部最優(yōu)),但其實(shí)拿2個(gè)2斤的蘋(píng)果能剛好裝4斤,所以并沒(méi)有得到最多蘋(píng)果(整體最優(yōu))。
二 什么問(wèn)題適合用貪心算法
對(duì)于一個(gè)詳細(xì)的問(wèn)題,怎么知道是否可用貪心算法解此問(wèn)題,以及是否能得到問(wèn)題的最優(yōu)解? 我們能夠依據(jù)貪心法的倆個(gè)重要的性質(zhì)去證明:貪心選擇性質(zhì)和最優(yōu)子結(jié)構(gòu)性質(zhì)。
貪心選擇:什么叫貪心選擇?從字義上就是貪心也就是目光短線。貪圖眼前利益。在算法中就是僅僅依據(jù)當(dāng)前已有的信息就做出選擇,并且以后都不會(huì)改變這次選擇。(這是和動(dòng)態(tài)規(guī)劃法的主要差別)所以對(duì)于一個(gè)詳細(xì)問(wèn)題。要確定它是否具有貪心選擇性質(zhì),必須證明每做一步貪心選擇是否終于導(dǎo)致問(wèn)題的總體最優(yōu)解
最優(yōu)子結(jié)構(gòu):當(dāng)一個(gè)問(wèn)題的最優(yōu)解包括其子問(wèn)題的最優(yōu)解時(shí),稱(chēng)此問(wèn)題具有最優(yōu)子結(jié)構(gòu)性質(zhì)。這個(gè)性質(zhì)和動(dòng)態(tài)規(guī)劃法的一樣,是可用動(dòng)態(tài)規(guī)劃算法或貪心算法求解的關(guān)鍵特征。
區(qū)分動(dòng)態(tài)規(guī)劃:動(dòng)態(tài)規(guī)劃算法通常以自底向上的方式解各子問(wèn)題,是遞歸過(guò)程;貪心算法則通常以自頂向下的方式進(jìn)行,以迭代的方式作出相繼的貪心選擇,每作一次貪心選擇就將所求問(wèn)題簡(jiǎn)化為規(guī)模更小的子問(wèn)題。以遍歷二叉樹(shù)為例:貪心算法是從上到下僅僅進(jìn)行深度搜索,也就是說(shuō)它從根節(jié)點(diǎn)一口氣走到底,它的代價(jià)取決于子問(wèn)題的數(shù)目,也就是樹(shù)的高度,每次在當(dāng)前問(wèn)題的狀態(tài)上作出的選擇都是1,不進(jìn)行廣度搜索,所以它得到的解不一定是最優(yōu)解,很可能是近似最優(yōu)解;而動(dòng)態(tài)規(guī)劃算法在最優(yōu)子結(jié)構(gòu)的前提下,從樹(shù)的葉子節(jié)點(diǎn)開(kāi)始向上進(jìn)行搜索,而且每一步都依據(jù)葉子節(jié)點(diǎn)的當(dāng)前問(wèn)題的狀況作出選擇,從而作出最優(yōu)決策,所以它的代價(jià)是子問(wèn)題的個(gè)數(shù)和可選擇的數(shù)目,它得到的解一定是最優(yōu)解。
三 貪心算法的求解過(guò)程?
一般求解過(guò)程:
代碼實(shí)現(xiàn):
Greedy(C) //C是問(wèn)題的輸入集合即候選集合
{
??? S={ }; //初始解集合為空集
??? while(not solve(S)) //集合S沒(méi)有構(gòu)成問(wèn)題的一個(gè)解
??? {
?????? x=select(C); //在候選集合C中做貪心選擇
?????? if availabe(S, x) //推斷集合S中增加x后的解是否可行
????????? S=S+{x};
????????? C=C-{x};
??? }
??? return S;
}
四 經(jīng)典案例分析?
[活動(dòng)選擇問(wèn)題]這是《算法導(dǎo)論》上的一個(gè)案例,也是一個(gè)非常經(jīng)典的問(wèn)題:
學(xué)校只有一個(gè)教室,下面表格i代表活動(dòng)的編號(hào),s代表活動(dòng)開(kāi)始時(shí)間,f代表活動(dòng)結(jié)束時(shí)間,現(xiàn)在問(wèn)題是怎么安排這些活動(dòng)使得盡量多的活動(dòng)能不沖突的舉行?
| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| s[i] | 1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 | 8 | 2 | 12 |
| f[i] | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
表 1.1 活動(dòng)時(shí)間表
用動(dòng)態(tài)規(guī)劃的話(huà)較麻煩:假設(shè)我已經(jīng)知道第k個(gè)活動(dòng)是活動(dòng)序列之一,那么又把1到k和k到11看做兩個(gè)子問(wèn)題繼續(xù)分下去。
用貪心算法的話(huà)很簡(jiǎn)單:活動(dòng)越早結(jié)束,剩余的時(shí)間越多,那就找到最早結(jié)束的那個(gè)活動(dòng),找到后在剩下的活動(dòng)中再找最早結(jié)束的,最后輸出找到的總數(shù)。
貪心選擇示例:
圖 1.1 貪心選擇圖
流程圖示例:
圖 1.2 思路流程圖
代碼:
#include<bits/stdc++.h> 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; } }
雖然貪心算法的思想簡(jiǎn)單,但是貪心法不保證能得到問(wèn)題的最優(yōu)解,如果得不到最優(yōu)解,那就不是我們想要的東西了,比如常見(jiàn)的背包問(wèn)題就不能使用貪心算法,下面舉個(gè)例子:
[0-1背包問(wèn)題]有一個(gè)背包,背包容量是M=150。有7個(gè)物品,有著各自的重量和價(jià)值。現(xiàn)要求盡可能讓裝入背包中的物品總價(jià)值最大,但不能超過(guò)總?cè)萘俊?/p>
物品 A ?B ?C ?D ?E ?F ?G
重量 35 30 60 50 40 10 25
價(jià)值 10 40 30 50 35 40 30
分析:
目標(biāo)函數(shù):∑pi最大
約束條件是裝入的物品總重量不超過(guò)背包容量:∑wi<=M( M=150)
3種貪心策略:
(1)每次挑選價(jià)值最大的物品裝入背包,是否能得到最優(yōu)解?
(2)每次挑選所占重量最小的物品裝入背包,是否能得到最優(yōu)解?
(3)每次選取單位重量?jī)r(jià)值最大的物品裝入背包,是否能得到最優(yōu)解?
證明:
一般來(lái)說(shuō),貪心算法的證明圍繞著:整個(gè)問(wèn)題的最優(yōu)解一定由在貪心策略中存在的子問(wèn)題的最優(yōu)解得來(lái)的。
對(duì)于例題中的3種貪心策略,都是無(wú)法被證明的,解釋如下:
(1)貪心策略:選取價(jià)值最大者。反例:
W=30
物品:A ?B ?C
重量:28 12 12
價(jià)值:30 20 20
根據(jù)策略,首先選取物品A,接下來(lái)就無(wú)法再選取了,可是,選取B、C則更好。
(2)貪心策略:選取重量最小者。它的反例與第一種策略的反例差不多。
(3)貪心策略:選取單位重量?jī)r(jià)值最大者。反例:
W=30
物品:A ?B ?C
重量:28 20 10
價(jià)值:28 20 10
根據(jù)策略,三種物品單位重量?jī)r(jià)值一樣,程序無(wú)法依據(jù)現(xiàn)有策略作出判斷,如果選擇物品A,則答案錯(cuò)誤。
但如果現(xiàn)在考慮這樣一種背包問(wèn)題:在選擇物品i裝入背包時(shí),可以選擇物品的一部分,而不一定要全部裝入背包,即可以分割物品,這時(shí)便可以使用貪心算法求解了。計(jì)算每種物品的單位重量?jī)r(jià)值作為貪心選擇的依據(jù)指標(biāo),選擇單位重量?jī)r(jià)值最高的物品,將盡可能多的該物品裝入背包,依此策略一直地進(jìn)行下去,直到背包裝滿(mǎn)為止。在之前的0-1背包問(wèn)題中三種貪心策略之所以不能得到最優(yōu)解的原因是貪心選擇無(wú)法保證最終能將背包裝滿(mǎn),部分閑置的背包空間使每公斤背包空間的價(jià)值降低了。以HDUOJ-1009為例:
[老鼠交易問(wèn)題]老鼠準(zhǔn)備了M磅的貓糧,準(zhǔn)備與守衛(wèi)他最喜歡的食物JavaBean的倉(cāng)庫(kù)的貓交易。該倉(cāng)庫(kù)有N個(gè)房間。第i個(gè)房間包含J[i]磅的JavaBeans并且需要F[i]磅的貓糧。老鼠不需要為房間里的所有JavaBeans進(jìn)行交易,相反,如果他支付F[i]*a%磅的貓糧,他會(huì)得到J[i]*a%磅的JavaBeans。現(xiàn)在要求出老鼠能得到的最多的JavaBean的數(shù)量。
分析:這道題其實(shí)就是一個(gè)0-1背包問(wèn)題,只不過(guò)物品可以被分割的放進(jìn)背包中,所以我們的貪心策略當(dāng)然是選取單位數(shù)量?jī)r(jià)值最大者放進(jìn)背包。
代碼:
#include<bits/stdc++.h> using namespace std;struct jb {double j,f,avg;//用于保存每個(gè)房間的信息 };
bool cmp(jb x,jb y) {return x.avg<y.avg;//定義排序規(guī)則,即均值小的排在前面 }
int main() {double n;int m;while(cin>>n>>m&&n!=-1){jb s[1001];for(int i=0;i<m;i++){cin>>s[i].j>>s[i].f;s[i].avg=s[i].j/s[i].f;}sort(s,s+m,cmp);double sum=0;for(int i=m-1;i>=0;--i)//從均值最大的房間開(kāi)始兌換 {if(n>=s[i].f)//如果剩余的貓糧多余房間所需貓糧 {n-=s[i].f;sum+=s[i].j;}else if(n<s[i].f&&n>0)//如果剩余貓糧少于房間所需貓糧 {sum+=s[i].avg*n;break;}}printf("%.3lf\n",sum); } }
[小船過(guò)河問(wèn)題]有n個(gè)人想過(guò)河,每個(gè)人過(guò)河都有自己的過(guò)河時(shí)間,但只有一只小船,最多只能裝2個(gè)人,每一次過(guò)河,過(guò)河時(shí)間為用時(shí)最多的那人過(guò)河時(shí)間,如果還有人沒(méi)有過(guò)河,那么過(guò)去一個(gè)用時(shí)最少的送回船。問(wèn)n人過(guò)河最少要多少時(shí)間。
分析:POJ-1700是一道經(jīng)典的貪心算法例題,在這道題中可以先將所有人過(guò)河所需的時(shí)間按照升序排序,我們考慮把單獨(dú)過(guò)河所需要時(shí)間最多的兩個(gè)旅行者送到對(duì)岸去,有兩種貪心策略:
(1)最快的和次快的過(guò)河,然后最快的將船劃回來(lái);最慢的和次慢的過(guò)河,然后次快的將船劃回來(lái),所需時(shí)間為:t[0]+2*t[1]+t[n-1]
(2)最快的和最慢的過(guò)河,然后最快的將船劃回來(lái);最快的和次慢的過(guò)河,然后最快的將船劃回來(lái),所需時(shí)間為:2*t[0]+t[n-2]+t[n-1]
代碼:
#include<bits/stdc++.h> using namespace std;int main() {int a[1000],t,n,sum;cin>>t;while(t--){cin>>n;sum=0;for(int i=0;i<n;i++)cin>>a[i];while(n>3){
sum=min(sum+a[1]+a[0]+a[n-1]+a[1],sum+a[n-1]+a[0]+a[n-2]+a[0]);//選則兩種貪心策略中數(shù)值較小的一個(gè)n-=2;//一次可以送走兩個(gè)人 }if(n==3)sum+=a[0]+a[1]+a[2];//當(dāng)剩余人數(shù)等于3時(shí),0和1過(guò)河,0反回,然后0和2過(guò)河else if(n==2)sum+=a[1];else sum+=a[0];printf("%d\n",sum);} }
五 結(jié)論?
貪心算法既然被列為五大常用算法之一,肯定在算法中占據(jù)了不小的地位,雖然貪心思想比較簡(jiǎn)單,但熟練應(yīng)用貪心思想絕對(duì)是我們必備的技能,在一些最優(yōu)化問(wèn)題中常常需要運(yùn)用貪心思想,像數(shù)據(jù)結(jié)構(gòu)中的Huffman編碼,Dijkstra算法,Kruskal算法和Prim算法都能看見(jiàn)貪心思想的身影,所以千萬(wàn)一定不能小覷貪心算法!
?
轉(zhuǎn)載于:https://www.cnblogs.com/wjw2018/p/9340526.html
總結(jié)
以上是生活随笔為你收集整理的五大常用算法之贪心算法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 回归初心
- 下一篇: [Leetcode][第392题][JA