第十六章:貪心算法--活動選擇問題html
前言:貪心算法也是用來解決最優化問題,將一個問題分紅子問題,在如今子問題最優解的時,選擇當前看起來是最優的解,指望經過所作的局部最優選擇來產生一個全局最優解。書中先從活動選擇問題來引入貪心算法,分別採用動態規劃方法和貪心算法進行分析。本篇筆記給出活動選擇問題的詳細分析過程,並給出詳細的實現代碼進行測試驗證。關於貪心算法的詳細分析過程,下次在討論。ios
一、活動選擇問題描述算法
有一個須要使用每一個資源的n個活動組成的集合S= {a1,a2,···,an },資源每次只能由一個活動使用。每一個活動ai都有一個開始時間si和結束時間fi,且 0≤si<fi<∞ 。一旦被選擇後,活動ai就佔據半開時間區間[si,fi)。若是[si,fi]和[sj,fj]互不重疊,則稱ai和aj兩個活動是兼容的。該問題就是要找出一個由互相兼容的活動組成的最大子集。例以下圖所示的活動集合S,其中各項活動按照結束時間單調遞增排序。編程
從圖中能夠看出S中共有11個活動,最大的相互兼容的活動子集爲:{a1,a4,a8,a11,}和{a2,a4,a9,a11}。數組
二、動態規劃解決過程安全
(1)活動選擇問題的最優子結構數據結構
定義子問題解空間Sij是S的子集,其中的每一個得到都是互相兼容的。即每一個活動都是在ai結束以後開始,且在aj開始以前結束。學習
爲了方便討論和後面的計算,添加兩個虛構活動a0和an+1,其中f0=0,sn+1=∞。測試
結論:當i≥j時,Sij爲空集。優化
若是活動按照結束時間單調遞增排序,子問題空間被用來從Sij中選擇最大兼容活動子集,其中0≤i<j≤n+1,因此其餘的Sij都是空集。
最優子結構爲:假設Sij的最優解Aij包含活動ak,則對Sik的解Aik和Skj的解Akj一定是最優的。
經過一個活動ak將問題分紅兩個子問題,下面的公式能夠計算出Sij的解Aij。
(2)一個遞歸解
設c[i][j]爲Sij中最大兼容子集中的活動數目,當Sij爲空集時,c[i][j]=0;當Sij非空時,若ak在Sij的最大兼容子集中被使用,則則問題Sik和Skj的最大兼容子集也被使用,故可獲得c[i][j] = c[i][k]+c[k][j]+1。
當i≥j時,Sij一定爲空集,不然Sij則須要根據上面提供的公式進行計算,若是找到一個ak,則Sij非空(此時知足fi≤sk且fk≤sj),找不到這樣的ak,則Sij爲空集。
c[i][j]的完整計算公式以下所示:
(3)最優解計算過程
根據遞歸公式,採用自底向下的策略進行計算c[i][j],引入複雜數組ret[n][n]保存中間劃分的k值。程序實現以下所示:
void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]) { int i,j,k; int temp; //當i>=j時候,子問題的解爲空,即c[i][j]=0 for(j=1;j<=N;j++) for(i=j;i<=N;i++) c[i][j] = 0; //當i<j時,須要尋找子問題的最優解,找到一個k使得將問題分紅兩部分 for(j=2;j<=N;j++) for(i=1;i<j;i++) { //尋找k,將問題分紅兩個子問題c[i][k]、c[k][j] for(k=i+1;k<j;k++) if(s[k] >= f[i] && f[k] <= s[j]) //判斷k活動是否知足兼容性 { temp = c[i][k]+c[k][j]+1; if(c[i][j] < temp) { c[i][j] =temp; ret[i][j] = k; } } } }
(4)構造一個最優解集合
根據第三保存的ret中的k值,遞歸調用輸出得到集合。採用動態規劃方法解決上面的例子,完整程序以下所示:
#include <stdio.h> #include <stdlib.h> #define N 11 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]); void trace_route(int ret[N+1][N+1],int i,int j); int main() { int s[N+1] = {-1,1,3,0,5,3,5,6,8,8,2,12}; int f[N+1] = {-1,4,5,6,7,8,9,10,11,12,13,14}; int c[N+1][N+1]={0}; int ret[N+1][N+1]={0}; int i,j; dynamic_activity_selector(s,f,c,ret); printf("c[i][j]的值以下所示:\n"); for(i=1;i<=N;i++) { for(j=1;j<=N;j++) printf("%d ",c[i][j]); printf("\n"); } //包括第一個和最後一個元素 printf("最大子集的個數爲: %d\n",c[1][N]+2); printf("ret[i][j]的值以下所示:\n"); for(i=1;i<=N;i++) { for(j=1;j<=N;j++) printf("%d ",ret[i][j]); printf("\n"); } printf("最大子集爲:{ a1 "); trace_route(ret,1,N); printf("a%d}\n",N); system("pause"); return 0; } void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]) { int i,j,k; int temp; //當i>=j時候,子問題的解爲空,即c[i][j]=0 for(j=1;j<=N;j++) for(i=j;i<=N;i++) c[i][j] = 0; //當i>j時,須要尋找子問題的最優解,找到一個k使得將問題分紅兩部分 for(j=2;j<=N;j++) for(i=1;i<j;i++) { //尋找k,將問題分紅兩個子問題c[i][k]、c[k][j] for(k=i+1;k<j;k++) if(s[k] >= f[i] && f[k] <= s[j]) //判斷k活動是否知足兼容性 { temp = c[i][k]+c[k][j]+1; if(c[i][j] < temp) { c[i][j] =temp; ret[i][j] = k; } } } } void trace_route(int ret[N+1][N+1],int i,int j) { if(i<j) { trace_route(ret,i,ret[i][j]); if(ret[i][j] != 0 ) printf("a%d ", ret[i][j]); } }
程序測試結果以下所示:
三、貪心算法解決過程
針對活動選擇問題,認真分析能夠得出如下定理:對於任意非空子問題Sij,設am是Sij中具備最先結束時間的活動,那麼:
(1)活動am在Sij中的某最大兼容活動子集中被使用。
(2)子問題Sim爲空,因此選擇am將使子問題Smj爲惟一可能非空的子問題。
有這個定理,就簡化了問題,使得最優解中只使用一個子問題,在解決子問題Sij時,在Sij中選擇最先結束時間的那個活動。
貪心算法自頂向下地解決每一個問題,解決子問題Sij,先找到Sij中最先結束的活動am,而後將am添加到最優解活動集合中,再來解決子問題Smj。
基於這種思想能夠採用遞歸和迭代進行實現。遞歸實現過程以下所示:
void recursive_activity_selector(int *s,int* f,int i,int n,int *ret) { int *ptmp = ret; int m = i+1; //在S i n中尋找第一個結束的活動 while(m<=n && s[m] < f[i]) m = m+1; if(m<=n) { *ptmp++ = m; //添加到結果中 recursive_activity_selector(s,f,m,n,ptmp); } }
迭代實現過程以下:
void greedy_activity_selector(int *s,int *f,int *ret) { int i,m; *ret++ = 1; i =1; for(m=2;m<=N;m++) if(s[m] >= f[i]) { *ret++ = m; i=m; } }
採用貪心算法實現上面的例子,完整代碼以下所示:
#include <stdio.h> #include <stdlib.h> #define N 11 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret); void greedy_activity_selector(int *s,int *f,int *ret); int main() { int s[N+1] = {-1,1,3,0,5,3,5,6,8,8,2,12}; int f[N+1] = {-1,4,5,6,7,8,9,10,11,12,13,14}; int c[N+1][N+1]={0}; int ret[N]={0}; int i,j; //recursive_activity_selector(s,f,0,N,ret); greedy_activity_selector(s,f,ret); printf("最大子集爲:{ "); for(i=0;i<N;i++) { if(ret[i] != 0) printf("a%d ",ret[i]); } printf(" }\n"); system("pause"); return 0; } void recursive_activity_selector(int *s,int* f,int i,int n,int *ret) { int *ptmp = ret; int m = i+1; //在i和n中尋找第一個結束的活動 while(m<=n && s[m] < f[i]) m = m+1; if(m<=n) { *ptmp++ = m; //添加到結果中 recursive_activity_selector(s,f,m,n,ptmp); } } void greedy_activity_selector(int *s,int *f,int *ret) { int i,m; *ret++ = 1; i =1; for(m=2;m<=N;m++) if(s[m] >= f[i]) { *ret++ = m; i=m; } }
程序測試結果以下所示:
四、總結
活動選擇問題分別採用動態規劃和貪心算法進行分析並實現。動態規劃的運行時間爲O(n^3),貪心算法的運行時間爲O(n)。動態規劃解決問題時全局最優解中必定包含某個局部最優解,但不必定包含前一個局部最優解,所以須要記錄以前的全部最優解。貪心算法的主要思想就是對問題求解時,老是作出在當前看來是最好的選擇,產生一個局部最優解。
第十六章:揹包問題:動態規劃求解
一、前言
前段時間忙着搞畢業論文,看書效率不高,致使博客一個多月沒有更新了。前段時間真是有些墮落啊,混日子的感受,不多不爽。今天開始繼續看算法導論。今天繼續學習動態規劃和貪心算法。首先簡單的介紹一下動態規劃與貪心算法的各自特色及其區別。而後針對0-1揹包問題進行討論。最後給出一個簡單的測試例子,聯繫動態規劃實現0-1揹包問題。
二、動態規劃與貪心算法
關於動態規劃的總結請參考http://www.cnblogs.com/Anker/archive/2013/03/15/2961725.html。這裏重點介紹一下貪心算法的過程。貪心算法是經過一系列的選擇來給出某一個問題的最優解,每次選擇一個當前(看起來是)最佳的選擇。貪心算法解決問題的步驟爲:
(1)決定問題的最優子結構
(2)設計出一個遞歸解
(3)證實在遞歸的任一階段,最優選擇之一老是貪心選擇。保證貪心選擇老是安全的。
(4)證實經過貪心選擇,全部子問題(除一個意外)都爲空。
(5)設計出一個實現貪心策略的遞歸算法。
(6)將遞歸算法轉換成迭代算法。
何時才能使用貪心算法的呢?書中給出了貪心算法的兩個性質,只有最優化問題知足這些性質,就可採用貪心算法解決問題。
(1)貪心選擇性質:一個全局最優解能夠經過舉辦最優解(貪心)選擇來達到。即:當考慮作選擇時,只考慮對當前問題最佳的選擇而不考慮子問題的結果。而在動態規劃中,每一步都要作出選擇,這些選擇依賴於子問題的解。動態規劃通常是自底向上,從小問題到大問題。貪心算法一般是自上而下,一個一個地作貪心選擇,不斷地將給定的問題實例規約爲更小的子問題。
(2)最優子結構:問題的一個最優解包含了其子問題的最優解。
動態規劃與貪心的區別:
貪心算法:
(1)貪心算法中,做出的每步貪心決策都沒法改變,由於貪心策略是由上一步的最優解推導下一步的最優解,而上一部以前的最優解則不做保留;
(2)由(1)中的介紹,能夠知道貪心法正確的條件是:每一步的最優解必定包含上一步的最優解。
動態規劃算法:
(1)全局最優解中必定包含某個局部最優解,但不必定包含前一個局部最優解,所以須要記錄以前的全部最優解 ;
(2)動態規劃的關鍵是狀態轉移方程,即如何由以求出的局部最優解來推導全局最優解 ;
(3)邊界條件:即最簡單的,能夠直接得出的局部最優解。
三、0-1揹包問題描述
有一個竊賊在偷竊一家商店時發現有n件物品,第i件物品價值爲vi元,重量爲wi,假設vi和wi都爲整數。他但願帶走的東西越值錢越好,但他的揹包中之多隻能裝下W磅的東西,W爲一整數。他應該帶走哪幾樣東西?
0-1揹包問題中:每件物品或被帶走,或被留下,(須要作出0-1選擇)。小偷不能只帶走某個物品的一部分或帶走兩次以上同一個物品。
部分揹包問題:小偷能夠只帶走某個物品的一部分,沒必要作出0-1選擇。
四、0-1揹包問題解決方法
0-1揹包問題是個典型舉辦子結構的問題,可是隻能採用動態規劃來解決,而不能採用貪心算法。由於在0-1揹包問題中,在選擇是否要把一個物品加到揹包中,必須把該物品加進去的子問題的解與不取該物品的子問題的解進行比較。這種方式造成的問題致使了許多重疊子問題,知足動態規劃的特徵。動態規劃解決0-1揹包問題步驟以下:
0-1揹包問題子結構:選擇一個給定物品i,則須要比較選擇i的造成的子問題的最優解與不選擇i的子問題的最優解。分紅兩個子問題,進行選擇比較,選擇最優的。
0-1揹包問題遞歸過程:設有n個物品,揹包的重量爲w,C[i][w]爲最優解。即:
課後習題給出了僞代碼:
五、編程實現
如今給定3個物品,揹包的容量爲50磅。物品1重10磅,價值爲60,物品2重20磅,價值爲100,物品3重30磅,價值爲120。採用動態規劃能夠知道最優解爲220,選擇物品2和3。採用C++語言實現以下:
#include <iostream> using namespace std; //物品數據結構 typedef struct commodity { int value; //價值 int weight; //重量 }commodity; const int N = 3; //物品個數 const int W = 50; //揹包的容量 //初始物品信息 commodity goods[N+1]={{0,0},{60,10},{100,20},{120,30}}; int select[N+1][W+1]; int max_value(); int main() { int maxvalue = max_value(); cout<<"The max value is: "; cout<<maxvalue<<endl; int remainspace = W; //輸出所選擇的物品列表: for(int i=N; i>=1; i--) { if (remainspace >= goods[i].weight) { if ((select[i][remainspace]-select[i-1][remainspace-goods[i].weight]==goods[i].value)) { cout << "item " << i << " is selected!" << endl; remainspace = remainspace - goods[i].weight;//若是第i個物品被選擇,那麼揹包剩餘容量將減去第i個物品的重量 ; } } } return 0; } int max_value() { //初始沒有物品時候,揹包的價值爲0 for(int w=1;w<=W;++w) select[0][w] = 0; for(int i=1;i<=N;++i) { select[i][0] = 0; //揹包容量爲0時,最大價值爲0 for(int w=1;w<=W;++w) { if(goods[i].weight <= w) //當前物品i的重量小於等於w,進行選擇 { if( (goods[i].value + select[i-1][w-goods[i].weight]) > select[i-1][w]) select[i][w] = goods[i].value + select[i-1][w-goods[i].weight]; else select[i][w] = select[i-1][w]; } else //當前物品i的重量大於w,不選擇 select[i][w] = select[i-1][w]; } } return select[N][W]; //最終求得最大值 }
程序測試結果以下: