從 活動選擇問題 看動態規划和貪心算法的區別與聯系


這篇文章主要用來記錄我對《算法導論》 貪心算法一章中的“活動選擇問題”的動態規划求解和貪心算法求解 的思路和理解。

主要涉及到以下幾個方面的內容:

①什么是活動選擇問題---粗略提下,詳細請參考《算法導論》

②活動選擇問題的DP(Dynamic programming)求解--DP求解問題的思路

③活動選擇問題的貪心算法求解

④為什么這個問題可以用貪心算法求解?

⑤動態規划與貪心算法的一些區別與聯系

⑥活動選擇問題的DP求解的JAVA語言實現以及時間復雜度分析

⑦活動選擇問題的Greedy算法JAVA實現和時間復雜度分析

⑧一些有用的參考資料

 

①活動選擇問題

給定N個活動,以及它們的開始時間和結束時間,求N個活動中,最大兼容的活動個數。比如:

活動 i:             1     2     3      4.....

開始時間 si:     1      3     0      5....

結束時間 fi:     4      5     6       7.....

活動1的開始時間s(1)=1,結束時間f(1)=4,它與活動2是不兼容的。因為,活動1還沒有結束,活動2就開始了(s(2) < f(1))。

活動2 與 活動4 是兼容的。因為,活動2的進行區間是[3,5) 而活動4的進行區間是[5,7)

目標是:在N個活動中,找出最大兼容的活動個數。

 

②活動選擇問題的DP(Dynamic programming)求解

1)建模

活動 i 用 a(i)來表示,開始時間用 s(i)表示,結束時間用 f(i)表示,所有活動的集合為S

定義一個合適的子問題空間,設 S(i,j) 是與 a(i) 和 a(j)兼容的活動集合。S(i,j)={a(k), a(k) belongs to S: f(i)<=s(k)<f(k)<=s(j)}

2)問題一般化(不是很理解)

這里第一個活動和最后一個活動有點特殊。為了完整表示問題,構造兩個虛擬的活動: a(0) 和 a(n+1)

其中,s(0)=f(0)=0,s(n+1)=f(n+1)=Integer.MAX_VALUE

於是,S=S(0,n+1),從N個活動中找出最大兼容的活動,就轉化成了求解 S(0,n+1)集合中包含的最多元素個數

3)子問題分析

假設所有的活動都按結束時間遞增排序。子問題空間就是 從S(i,j)中選擇最大兼容活動子集,即max{S(i,j)}

max{S(i,j)}表示與 a(i) a(j) 兼容的最大活動集合。稱為為S(i,j)的解

假設 a(k)是 S(i,j)的解包含的一個活動。S(i,j)就分解為 max{S(i,k)} + max{S(k,j)}+1

從這里可以看到,將原問題分解成了兩個子問題。原問題就是:求解與活動 a(i) a(j) 兼容的最大活動個數,即max{S(i,j)}

而子問題則是:max{S(i,k)}  和  max{S(k,j)}

設A(i,j)就是S(i,j)的解。那么,A(i,j)=A(i,k) U A(k,j) U {a(k)}

A(0,n+1)就是我們所求的整個問題的最優解。

4)子問題的 選擇個數 分析

設c[i,j]為S(i,j)中最大兼容子集中的活動數,S(i,j)為空集時,c[i,j]=0,這是顯而易見的。因為S(i,j)中都沒有活動嘛,更別談什么兼容活動了呀。

若 i>=j,c[i,j]=0。這個也很好理解,因為它不符合常識。因為,我們假設活動是以結束時間來遞增排序的,在S(i,j)中,是f(i)<s(j)的。那 i 就不會大於 j

畢竟一個活動它不可能 即在 某個活動之前結束,又在該活動之后開始。哈哈。。。。。

前面提到 :假設 a(k)是 S(i,j)的解包含的一個活動。S(i,j)就分解為 max{S(i,k)} + max{S(k,j)}+1

這意味着,求S(i,j)的最優解,就需要知道 S(i,k) 和 S(k,j) 的最優解。那關鍵是怎么知道 S(i,k) 和 S(k,j) 的最優解呢?

答案是:一個 一個 地嘗試。k 的取值范圍是 (i,j),遍歷(i,j)內所有的值,計算 S(i,k) 和 S(k,j)的解。就可以找到S(i,j)的最優解了。

 因此,當S(i,j)不為空時,c[i,j] = max{c[i,k] + c[k,j] + 1}  其中, k belongs to (i,j)  a(k) belongs to S(i,j)

下面,就是DP中的狀態轉移方程(遞歸表達式),根據它,就可以寫代碼實現了。

從上面分析可以看出:原問題分解成了兩個子問題,要解決原問題,一共有 j-i+1中選擇,然后一 一遍歷求出所有的選擇。這就是動態規划的特點,先分析最優子問題,然后再做選擇。

 

③活動選擇問題的貪心算法求解

所謂貪心算法,就是每次在做選擇時,總是先選擇具有相同特征的那個解,即“貪心”解。在這里,“貪心”的那個解則是: 結束時間最早的那個活動

具體步驟是怎樣的呢?

第一步:先對活動按照結束時間進行排序。因為我們總是優先選擇結束時間最早的活動的嘛。排序之后,方便選擇嘛。。。

第二步:按照貪心原則 選中一個活動,然后排除 所有與該活動 有沖突的活動。

第三步:繼續選擇下一個活動。其實,第二步與第三步合起來就是:每次都選結束時間最早的活動,但是后面選擇的活動不能與前面選擇的活動有沖突。

從這里可以看出,貪心算法是在原問題上先做貪心選擇,然后得到一個子問題,再求解子問題。(求解子問題的過程,就是一個不斷貪心選擇的過程)

 

④為什么這個問題可以用貪心算法求解?

看了貪心算法之后,就會有疑問?憑什么這樣選就能得到最優解啊?或者說,這樣做到底對不對?

別急嘛,我們可以用數學來證明這樣做是正確的。而且從這個證明過程中,可以窺出動態規划與貪心算法的區別。

對於活動選擇問題而言:當可用貪心算法解時,貪心的效率要比動態規划高。為什么要高呢?后面再詳細講。

這個證明具體可參考《算法導論》上的證明。它的大致證明過程就是:

當選擇了貪心解時(結束時間最小的活動),也是將原問題划分成了兩個子問題,但是其中一個子問題是空的,而我們只需要考慮另一個非空的子問題就可以了。

具體而言就是:假設 a(m) 是 S(i,j)中具有最早結束時間的那個活動,那按照我們的貪心選擇,我們肯定會選擇a(m)的嘛。選了a(m)之后,就將問題分解成了兩個子問題:S(i,m) 和 S(m,j)。前面提到,活動是按結束時間排序了的,而現在a(m)又是最早結束的活動,因為,S(i,m)就是個空集,而我們只需要考慮S(m,j)

但是,這里有個重大的疑問還未解決---憑什么說 a(m) 就是 S(i,j)的最優解中的活動呢?或者說憑什么 活動m 就是最大兼容活動集合中的活動?

這里就用到經常用來證明貪心算法正確性的一個技巧---剪枝。關於這個技巧,可參考一篇博文:漫談算法(一)如何證明貪心算法是最優

對於活動選擇問題,咱就來簡要證明下吧。。。其實還是《算法導論》中講的證明,只不過我又復述一遍罷了。

慢着,我們要證明的是啥?再說一遍:憑什么說 a(m) 就是 S(i,j)的最優解中的活動呢?,我們證明的就是:a(m)是S(i,j)的最優解中的元素,即a(m)是S(i,j)最大兼容活動子集中的活動。

設A(i,j)是S(i,j)的最大兼容活動子集---也就是說,在所有與 活動a(i) 和 活動a(j) 相兼容的活動中,A(i,j)含有的活動個數最多。

將A(i,j)中的活動按結束時間遞增排序。設a(k)是A(i,j)中的第一個活動若a(k)=a(m),那沒話說了。a(m)就是a(k)嘛,那a(m)肯定在A(i,j)中噻

若a(k) != a(m),這說明A(i,j)中的第一個元素(活動)不是a(m)。那我們可以運用剪枝思想,剪掉A(i,j)中的第一個活動a(k),再把活動a(m)貼到A(i,j)里面去

這樣,A(i,j)中的活動個數還是沒有變化---少了個a(k),加了個a(m)啊

那么,可能你就會問了,憑什么能把 a(m)貼到 A(i,j)里面去啊?????我們可以這樣想想:a(k)是A(i,j)中的第一個活動,那為什么a(k)可以在A(i,j)中呢?

廢話!上面帶下划線且加粗的的都說了假設 a(k)是A(i,j)中的第一個活動了啊!!

其實,這不是本質 ,本質就是:a(k)是與 a(i) 和 a(j)兼容的活動啊,而且沒有和A(i,j)中的其他活動沖突啊!因為,S(i,j)的解 就是求與 a(i) 和 a(j)兼容的一組活動啊,而A(i,j)就是這樣的一組活動且它是最大的(活動個數最多),能夠放在A(i,j)中的活動,它一定是與a(i) 和 a(j) 兼容的。

那么,再回到a(m),a(m)同樣也具有 ”本質“ 中提到的兩個性質:❶a(m)是與a(i) 和 a(j) 兼容的活動  ❷a(m)沒有與A(i,j)中其他活動沖突。

下面來說明下為什么 a(m)沒有與A(i,j)中其他活動沖突?因為a(k)是沒有與A(i,j)中的其他活動沖突的,而a(m)又是S(i,j)中結束時間最早的活動

故:,完成時間:f(m)<f(k) ,a(m)都比a(k)更早完成,而a(k)都沒有與A(i,j)中的其他活動沖突,那a(m)就更不可能與A(i,j)中的其他活動沖突了。

整個思路就是:在證明中先考察一個全局最優解,然后證明可以對該解加以修改(比如運用“剪枝”技巧),使其采用貪心選擇(將貪心的那個選擇貼上去),這個選擇將原問題變成一個相似的、但更小的問題。

終於完成了證明。好累。

 

⑤動態規划與貪心算法的一些區別與聯系

這里只針對活動選擇問題作一下比較。其他的我也不懂。

a)動態規划是先分析子問題,再做選擇。而貪心算法則是先做貪心選擇,做完選擇后,生成了子問題,然后再去求解子問題。

b)從 a) 中可以看出,動態規划是自底向上解決問題,而貪心算法則是自頂向下解決問題。

c)動態規划每一步可能會產生多個子問題,而貪心算法每一步只會產生一個子問題。(比如這里的貪心算法產生了“二個”子問題,但是其中一個是空的。)

 

⑥活動選擇問題的DP求解的JAVA語言實現以及時間復雜度分析

 1 /**
 2      * //算法導論中活動選擇問題動態規划求解
 3      * @param s 活動的開始時間
 4      * @param f 活動的結束時間
 5      * @param n 活動數目
 6      * @return 最大兼容的活動個數
 7      */
 8     public static int maxCompatiableActivity(int[] s, int[] f, int n){
 9         int[][] c = new int[n + 2][n + 2];
10         
11         for(int j = 0; j <= n+1; j++)
12             for(int i = n+1; i >= j; i--)
13                 c[i][j] = 0;//if i>=j S(i,j)是空集合
14         
15         int maxTemp = 0;
16         for(int j = 1; j <= n+1; j++)
17         {
18             for(int i = 0; i < j; i++)//i < j
19             {
20                 for(int k = i+1; k < j; k++)// i< k <j
21                 {
22                     if(s[k] >= f[i] && f[k] <= s[j])//S(i,j)不空
23                     {
24                         if(c[i][k] + c[k][j] + 1 > maxTemp)
25                             maxTemp = c[i][k] + c[k][j] + 1;
26                     }
27                 }//inner for
28                 c[i][j] = maxTemp;
29                 maxTemp = 0;
30             }//media for
31         }//outer for
32         return c[0][n+1];
33     }

DP時間復雜度與問題的個數以及每個問題的選擇數 有關

比如這里的 S(i,j)一共大約有N^2個, 因為 1=<j<=N, 1=<i<j  ,這里求和大約是 (N^2)/2(對於S(i,j) i>j沒有實際意義嘛),每個S(i,j)一共有 j-i+1種 選擇

故時間復雜度為O(N^3)

 

⑦活動選擇問題的Greedy算法JAVA實現和時間復雜度分析

貪心算法即可以用遞歸實現,也可以用非遞歸實現。

 1 //貪心算法的遞歸解
 2     public static ArrayList<Integer> greedyActivitySelection(int[] s, int[] f, int i, int n, ArrayList<Integer> activities){
 3         //初始調用時 i = 0, 所以a(1)是必選的(注意:活動編號已經按結束時間排序)
 4         int m = i + 1;
 5         
 6         //s[m] < f[i] 意味着活動 a(m) 與 a(i)沖突了
 7         while(m <= n && s[m] < f[i])
 8             m++;//選擇下一個活動
 9         
10         if(m <= n){
11             activities.add(m);
12             greedyActivitySelection(s, f, m, n, activities);
13         }
14         return activities;
15     }
16     
17     //貪心算法的非遞歸解, assume f[] has been sorted and actId 0/n+1 is virtually added
18     public static ArrayList<Integer> greedyActivitySelection2(int[] s, int[] f, int n, ArrayList<Integer> acitivities){
19         //所有真正的活動(不包括 活動0和 活動n+1)中,結束時間最早的那個活動一定是最大兼容活動集合中的 活動.
20         int m = 1;
21         acitivities.add(m);
22         
23         for(int actId = 2; actId <= n; actId++){
24             if(s[actId] >= f[m])//actId的開始時間在 m 號活動之后.--actId 與 m 沒有沖突
25             {
26                 m = actId;
27                 acitivities.add(m);
28             }
29         }
30         return acitivities;
31     }

貪心算法的時間復雜度為O(N),why?你可以看代碼啊。只有一個循環啊。每個活動只會遍歷一次啊。

這里從理論上來分析下:因為對於貪心算法而言,每次只有一種選擇即貪心選擇,而DP中每個問題S(i,j)中 j-i+1種選擇。

貪心算法做出一次貪心選擇后,即選中某個活動后,活動個數減少1,即問題規模減少1。

 

⑧參考資料

https://www.zhihu.com/question/23995189

《背包九講》

http://www.cnblogs.com/hapjin/p/5572483.html

 

附完整代碼:

import java.util.ArrayList;
public class ActivitySelection {

    /**
     * //算法導論中活動選擇問題動態規划求解
     * @param s 活動的開始時間
     * @param f 活動的結束時間
     * @param n 活動數目
     * @return 最大兼容的活動個數
     */
    public static int maxCompatiableActivity(int[] s, int[] f, int n){
        int[][] c = new int[n + 2][n + 2];
        
        for(int j = 0; j <= n+1; j++)
            for(int i = n+1; i >= j; i--)
                c[i][j] = 0;//if i>=j S(i,j)是空集合
        
        int maxTemp = 0;
        for(int j = 1; j <= n+1; j++)
        {
            for(int i = 0; i < j; i++)//i < j
            {
                for(int k = i+1; k < j; k++)// i< k <j
                {
                    if(s[k] >= f[i] && f[k] <= s[j])//S(i,j)不空
                    {
                        if(c[i][k] + c[k][j] + 1 > maxTemp)
                            maxTemp = c[i][k] + c[k][j] + 1;
                    }
                }//inner for
                c[i][j] = maxTemp;
                maxTemp = 0;
            }//media for
        }//outer for
        return c[0][n+1];
    }
    
    //貪心算法的遞歸解
    public static ArrayList<Integer> greedyActivitySelection(int[] s, int[] f, int i, int n, ArrayList<Integer> activities){
        //初始調用時 i = 0, 所以a(1)是必選的(注意:活動編號已經按結束時間排序)
        int m = i + 1;
        
        //s[m] < f[i] 意味着活動 a(m) 與 a(i)沖突了
        while(m <= n && s[m] < f[i])
            m++;//選擇下一個活動
        
        if(m <= n){
            activities.add(m);
            greedyActivitySelection(s, f, m, n, activities);
        }
        return activities;
    }
    
    //貪心算法的非遞歸解, assume f[] has been sorted and actId 0/n+1 is virtually added
    public static ArrayList<Integer> greedyActivitySelection2(int[] s, int[] f, int n, ArrayList<Integer> acitivities){
        //所有真正的活動(不包括 活動0和 活動n+1)中,結束時間最早的那個活動一定是最大兼容活動集合中的 活動.
        int m = 1;
        acitivities.add(m);
        
        for(int actId = 2; actId <= n; actId++){
            if(s[actId] >= f[m])//actId的開始時間在 m 號活動之后.--actId 與 m 沒有沖突
            {
                m = actId;
                acitivities.add(m);
            }
        }
        return acitivities;
    }
    
    //for test purpose
    public static void main(String[] args) {
        //添加了 a(0) 和 a(n+1)活動. 其中s(0)=f(0)=0, s(n+1)=f(n+1)=Integer.MAX_VALUE
        int[] s = {0,1,3,0,5,3,5,6,8,8,2,12,Integer.MAX_VALUE};//start time
        int[] f = {0,4,5,6,7,8,9,10,11,12,13,14,Integer.MAX_VALUE};//finish time
        int n = 11;//活動的個數
        int result = maxCompatiableActivity(s, f, n);
        System.out.println("最大兼容活動個數: " + result);
        
        ArrayList<Integer> acts = new ArrayList<Integer>();
        greedyActivitySelection(s, f, 0, n, acts);
        for (Integer activityId : acts)
            System.out.print(activityId + " ");
        
        System.out.println();
        ArrayList<Integer> acts2 = new ArrayList<Integer>();
        greedyActivitySelection2(s, f, n, acts2);
        for (Integer activityId : acts2)
            System.out.print(activityId + " ");
    }
}

 


免責聲明!

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



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