《算法導論》讀書筆記之第16章 貪心算法—活動選擇問題


  前言:貪心算法也是用來解決最優化問題,將一個問題分成子問題,在現在子問題最優解的時,選擇當前看起來是最優的解,期望通過所做的局部最優選擇來產生一個全局最優解。書中先從活動選擇問題來引入貪心算法,分別采用動態規划方法和貪心算法進行分析。本篇筆記給出活動選擇問題的詳細分析過程,並給出詳細的實現代碼進行測試驗證。關於貪心算法的詳細分析過程,下次在討論。

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,a8a11}和{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值,遞歸調用輸出獲得集合。采用動態規划方法解決上面的例子,完整程序如下所示:

View Code
 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 }

采用貪心算法實現上面的例子,完整代碼如下所示:

View Code
 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)。動態規划解決問題時全局最優解中一定包含某個局部最優解,但不一定包含前一個局部最優解,因此需要記錄之前的所有最優解。貪心算法的主要思想就是對問題求解時,總是做出在當前看來是最好的選擇,產生一個局部最優解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM