《算法導論》讀書筆記(七)
前言:貪心算法也是用來解決最優化問題,將一個問題分成子問題,在現在子問題最優解的時,選擇當前看起來是最優的解,期望通過所做的局部最優選擇來產生一個全局最優解。書中先從活動選擇問題來引入貪心算法,分別采用動態規劃方法和貪心算法進行分析。本篇筆記給出活動選擇問題的詳細分析過程,並給出詳細的實現代碼進行測試驗證。關於貪心算法的詳細分析過程,下次在討論。
1、活動選擇問題描述
有一個需要使用每個資源的n個活動組成的集合S= {a1,a2,···,an },資源每次只能由一個活動使用。每個活動ai都有一個開始時間si和結束時間fi,且 0≤si<fi<∞ 。一旦被選擇後,活動ai
從圖中可以看出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)。動態規劃解決問題時全局最優解中一定包含某個局部最優解,但不一定包含前一個局部最優解,因此需要記錄之前的所有最優解。貪心算法的主要思想就是對問題求解時,總是做出在當前看來是最好的選擇,產生一個局部最優解。
《算法導論》讀書筆記之第16章 0-1背包問題—動態規劃求解
1、前言
前段時間忙著搞畢業論文,看書效率不高,導致博客一個多月沒有更新了。前段時間真是有些墮落啊,混日子的感覺,很少不爽。今天開始繼續看算法導論。今天繼續學習動態規劃和貪心算法。首先簡單的介紹一下動態規劃與貪心算法的各自特點及其區別。然後針對0-1背包問題進行討論。最後給出一個簡單的測試例子,聯系動態規劃實現0-1背包問題。
2、動態規劃與貪心算法
關於動態規劃的總結請參考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)邊界條件:即最簡單的,可以直接得出的局部最優解。
3、0-1背包問題描述
有一個竊賊在偷竊一家商店時發現有n件物品,第i件物品價值為vi元,重量為wi,假設vi和wi都為整數。他希望帶走的東西越值錢越好,但他的背包中之多只能裝下W磅的東西,W為一整數。他應該帶走哪幾樣東西?
0-1背包問題中:每件物品或被帶走,或被留下,(需要做出0-1選擇)。小偷不能只帶走某個物品的一部分或帶走兩次以上同一個物品。
部分背包問題:小偷可以只帶走某個物品的一部分,不必做出0-1選擇。
4、0-1背包問題解決方法
0-1背包問題是個典型舉辦子結構的問題,但是只能采用動態規劃來解決,而不能采用貪心算法。因為在0-1背包問題中,在選擇是否要把一個物品加到背包中,必須把該物品加進去的子問題的解與不取該物品的子問題的解進行比較。這種方式形成的問題導致了許多重疊子問題,滿足動態規劃的特征。動態規劃解決0-1背包問題步驟如下:
0-1背包問題子結構:選擇一個給定物品i,則需要比較選擇i的形成的子問題的最優解與不選擇i的子問題的最優解。分成兩個子問題,進行選擇比較,選擇最優的。
0-1背包問題遞歸過程:設有n個物品,背包的重量為w,C[i][w]為最優解。即:
課後習題給出了偽代碼:
5、編程實現
現在給定3個物品,背包的容量為50磅。物品1重10磅,價值為60,物品2重20磅,價值為100,物品3重30磅,價值為120。采用動態規劃可以知道最優解為220,選擇物品2和3。采用C++語言實現如下:
1 #include <iostream>
2 using namespace std;
3
4 //物品數據結構
5 typedef struct commodity
6 {
7 int value; //價值
8 int weight; //重量
9 }commodity;
10
11 const int N = 3; //物品個數
12 const int W = 50; //背包的容量
13
14 //初始物品信息
15 commodity goods[N+1]={{0,0},{60,10},{100,20},{120,30}};
16 int select[N+1][W+1];
17
18 int max_value();
19
20 int main()
21 {
22 int maxvalue = max_value();
23 cout<<"The max value is: ";
24 cout<<maxvalue<<endl;
25 int remainspace = W;
26 //輸出所選擇的物品列表:
27 for(int i=N; i>=1; i--)
28 {
29 if (remainspace >= goods[i].weight)
30 {
31 if ((select[i][remainspace]-select[i-1][remainspace-goods[i].weight]==goods[i].value))
32 {
33 cout << "item " << i << " is selected!" << endl;
34 remainspace = remainspace - goods[i].weight;//如果第i個物品被選擇,那麽背包剩余容量將減去第i個物品的重量 ;
35 }
36 }
37 }
38 return 0;
39 }
40 int max_value()
41 {
42 //初始沒有物品時候,背包的價值為0
43 for(int w=1;w<=W;++w)
44 select[0][w] = 0;
45 for(int i=1;i<=N;++i)
46 {
47 select[i][0] = 0; //背包容量為0時,最大價值為0
48 for(int w=1;w<=W;++w)
49 {
50 if(goods[i].weight <= w) //當前物品i的重量小於等於w,進行選擇
51 {
52 if( (goods[i].value + select[i-1][w-goods[i].weight]) > select[i-1][w])
53 select[i][w] = goods[i].value + select[i-1][w-goods[i].weight];
54 else
55 select[i][w] = select[i-1][w];
56 }
57 else //當前物品i的重量大於w,不選擇
58 select[i][w] = select[i-1][w];
59 }
60 }
61 return select[N][W]; //最終求得最大值
62 }
程序測試結果如下:
《算法導論》讀書筆記(七)