前言:貪心算法也是用來解決最優化問題,將一個問題分成子問題,在現在子問題最優解的時,選擇當前看起來是最優的解,期望通過所做的局部最優選擇來產生一個全局最優解。書中先從活動選擇問題來引入貪心算法,分別采用動態規划方法和貪心算法進行分析。本篇筆記給出活動選擇問題的詳細分析過程,並給出詳細的實現代碼進行測試驗證。關於貪心算法的詳細分析過程,下次在討論。
1、活動選擇問題描述
有一個需要使用每個資源的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}。
2、動態規划解決過程
(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值。程序實現如下所示:
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 }
程序測試結果如下所示:
3、貪心算法解決過程
針對活動選擇問題,認真分析可以得出以下定理:對於任意非空子問題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 }
程序測試結果如下所示:
4、總結
活動選擇問題分別采用動態規划和貪心算法進行分析並實現。動態規划的運行時間為O(n^3),貪心算法的運行時間為O(n)。動態規划解決問題時全局最優解中一定包含某個局部最優解,但不一定包含前一個局部最優解,因此需要記錄之前的所有最優解。貪心算法的主要思想就是對問題求解時,總是做出在當前看來是最好的選擇,產生一個局部最優解。