前言:貪心算法也是用來解決最優化問題,將一個問題分紅子問題,在如今子問題最優解的時,選擇當前看起來是最優的解,指望經過所作的局部最優選擇來產生一個全局最優解。書中先從活動選擇問題來引入貪心算法,分別採用動態規劃方法和貪心算法進行分析。本篇筆記給出活動選擇問題的詳細分析過程,並給出詳細的實現代碼進行測試驗證。關於貪心算法的詳細分析過程,下次在討論。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=∞。post
結論:當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值。程序實現以下所示:
1 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]) 2 { 3 int i,j,k; 4 int temp; 5 //當i>=j時候,子問題的解爲空,即c[i][j]=0 6 for(j=1;j<=N;j++) 7 for(i=j;i<=N;i++) 8 c[i][j] = 0; 9 //當i<j時,須要尋找子問題的最優解,找到一個k使得將問題分紅兩部分 10 for(j=2;j<=N;j++) 11 for(i=1;i<j;i++) 12 { 13 //尋找k,將問題分紅兩個子問題c[i][k]、c[k][j] 14 for(k=i+1;k<j;k++) 15 if(s[k] >= f[i] && f[k] <= s[j]) //判斷k活動是否知足兼容性 16 { 17 temp = c[i][k]+c[k][j]+1; 18 if(c[i][j] < temp) 19 { 20 c[i][j] =temp; 21 ret[i][j] = k; 22 } 23 } 24 } 25 }
(4)構造一個最優解集合
根據第三保存的ret中的k值,遞歸調用輸出得到集合。採用動態規劃方法解決上面的例子,完整程序以下所示:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define N 11 5 6 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]); 7 void trace_route(int ret[N+1][N+1],int i,int j); 8 9 int main() 10 { 11 int s[N+1] = {-1,1,3,0,5,3,5,6,8,8,2,12}; 12 int f[N+1] = {-1,4,5,6,7,8,9,10,11,12,13,14}; 13 int c[N+1][N+1]={0}; 14 int ret[N+1][N+1]={0}; 15 int i,j; 16 dynamic_activity_selector(s,f,c,ret); 17 printf("c[i][j]的值以下所示:\n"); 18 for(i=1;i<=N;i++) 19 { 20 for(j=1;j<=N;j++) 21 printf("%d ",c[i][j]); 22 printf("\n"); 23 } 24 //包括第一個和最後一個元素 25 printf("最大子集的個數爲: %d\n",c[1][N]+2); 26 printf("ret[i][j]的值以下所示:\n"); 27 for(i=1;i<=N;i++) 28 { 29 for(j=1;j<=N;j++) 30 printf("%d ",ret[i][j]); 31 printf("\n"); 32 } 33 printf("最大子集爲:{ a1 "); 34 trace_route(ret,1,N); 35 printf("a%d}\n",N); 36 system("pause"); 37 return 0; 38 } 39 40 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]) 41 { 42 int i,j,k; 43 int temp; 44 //當i>=j時候,子問題的解爲空,即c[i][j]=0 45 for(j=1;j<=N;j++) 46 for(i=j;i<=N;i++) 47 c[i][j] = 0; 48 //當i>j時,須要尋找子問題的最優解,找到一個k使得將問題分紅兩部分 49 for(j=2;j<=N;j++) 50 for(i=1;i<j;i++) 51 { 52 //尋找k,將問題分紅兩個子問題c[i][k]、c[k][j] 53 for(k=i+1;k<j;k++) 54 if(s[k] >= f[i] && f[k] <= s[j]) //判斷k活動是否知足兼容性 55 { 56 temp = c[i][k]+c[k][j]+1; 57 if(c[i][j] < temp) 58 { 59 c[i][j] =temp; 60 ret[i][j] = k; 61 } 62 } 63 } 64 } 65 66 void trace_route(int ret[N+1][N+1],int i,int j) 67 { 68 if(i<j) 69 { 70 trace_route(ret,i,ret[i][j]); 71 if(ret[i][j] != 0 ) 72 printf("a%d ", ret[i][j]); 73 } 74 }
程序測試結果以下所示:
三、貪心算法解決過程
針對活動選擇問題,認真分析能夠得出如下定理:對於任意非空子問題Sij,設am是Sij中具備最先結束時間的活動,那麼:
(1)活動am在Sij中的某最大兼容活動子集中被使用。
(2)子問題Sim爲空,因此選擇am將使子問題Smj爲惟一可能非空的子問題。
有這個定理,就簡化了問題,使得最優解中只使用一個子問題,在解決子問題Sij時,在Sij中選擇最先結束時間的那個活動。
貪心算法自頂向下地解決每一個問題,解決子問題Sij,先找到Sij中最先結束的活動am,而後將am添加到最優解活動集合中,再來解決子問題Smj。
基於這種思想能夠採用遞歸和迭代進行實現。遞歸實現過程以下所示:
1 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret) 2 { 3 int *ptmp = ret; 4 int m = i+1; 5 //在Sin中尋找第一個結束的活動 6 while(m<=n && s[m] < f[i]) 7 m = m+1; 8 if(m<=n) 9 { 10 *ptmp++ = m; //添加到結果中 11 recursive_activity_selector(s,f,m,n,ptmp); 12 } 13 }
迭代實現過程以下:
1 void greedy_activity_selector(int *s,int *f,int *ret) 2 { 3 int i,m; 4 *ret++ = 1; 5 i =1; 6 for(m=2;m<=N;m++) 7 if(s[m] >= f[i]) 8 { 9 *ret++ = m; 10 i=m; 11 } 12 }
採用貪心算法實現上面的例子,完整代碼以下所示:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define N 11 5 6 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret); 7 8 void greedy_activity_selector(int *s,int *f,int *ret); 9 10 int main() 11 { 12 int s[N+1] = {-1,1,3,0,5,3,5,6,8,8,2,12}; 13 int f[N+1] = {-1,4,5,6,7,8,9,10,11,12,13,14}; 14 int c[N+1][N+1]={0}; 15 int ret[N]={0}; 16 int i,j; 17 //recursive_activity_selector(s,f,0,N,ret); 18 greedy_activity_selector(s,f,ret); 19 printf("最大子集爲:{ "); 20 for(i=0;i<N;i++) 21 { 22 if(ret[i] != 0) 23 printf("a%d ",ret[i]); 24 } 25 printf(" }\n"); 26 system("pause"); 27 return 0; 28 } 29 30 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret) 31 { 32 int *ptmp = ret; 33 int m = i+1; 34 //在i和n中尋找第一個結束的活動 35 while(m<=n && s[m] < f[i]) 36 m = m+1; 37 if(m<=n) 38 { 39 *ptmp++ = m; //添加到結果中 40 recursive_activity_selector(s,f,m,n,ptmp); 41 } 42 } 43 44 void greedy_activity_selector(int *s,int *f,int *ret) 45 { 46 int i,m; 47 *ret++ = 1; 48 i =1; 49 for(m=2;m<=N;m++) 50 if(s[m] >= f[i]) 51 { 52 *ret++ = m; 53 i=m; 54 } 55 }
程序測試結果以下所示:
四、總結
活動選擇問題分別採用動態規劃和貪心算法進行分析並實現。動態規劃的運行時間爲O(n^3),貪心算法的運行時間爲O(n)。動態規劃解決問題時全局最優解中必定包含某個局部最優解,但不必定包含前一個局部最優解,所以須要記錄以前的全部最優解。貪心算法的主要思想就是對問題求解時,老是作出在當前看來是最好的選擇,產生一個局部最優解。