1. 程式人生 > >五大經典演算法的分析

五大經典演算法的分析

原連結:https://www.cnblogs.com/steven_oyj/category/246990.html  若侵權告知立刪!

一、分治演算法

一、基本概念

  在電腦科學中,分治法是一種很重要的演算法。字面上的解釋是“分而治之”,就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。這個技巧是很多高效演算法的基礎,如排序演算法(快速排序,歸併排序),傅立葉變換(快速傅立葉變換)……

   任何一個可以用計算機求解的問題所需的計算時間都與其規模有關。問題的規模越小,越容易直接求解,解題所需的計算時間也越少。例如,對於n個元素的排序問題,當n=1時,不需任何計算。n=2時,只要作一次比較即可排好序。n=3時只要作3次比較即可,…。而當n較大時,問題就不那麼容易處理了。要想直接解決一個規模較大的問題,有時是相當困難的。


二、基本思想及策略

   分治法的設計思想是:將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。

   分治策略是:對於一個規模為n的問題,若該問題可以容易地解決(比如說規模n較小)則直接解決,否則將其分解為k個規模較小的子問題,這些子問題互相獨立且與原問題形式相同,遞迴地解這些子問題,然後將各子問題的解合併得到原問題的解。這種演算法設計策略叫做分治法。

   如果原問題可分割成k個子問題,1<k≤n,且這些子問題都可解並可利用這些子問題的解求出原問題的解,那麼這種分治法就是可行的。由分治法產生的子問題往往是原問題的較小模式,這就為使用遞迴技術提供了方便。在這種情況下,反覆應用分治手段,可以使子問題與原問題型別一致而其規模卻不斷縮小,最終使子問題縮小到很容易直接求出其解。這自然導致遞迴過程的產生。分治與遞迴像一對孿生兄弟,經常同時應用在演算法設計之中,並由此產生許多高效演算法。


三、分治法適用的情況

    分治法所能解決的問題一般具有以下幾個特徵:

    1) 該問題的規模縮小到一定的程度就可以容易地解決

    2) 該問題可以分解為若干個規模較小的相同問題,即該問題具有最優子結構性質。

    3) 利用該問題分解出的子問題的解可以合併為該問題的解;

    4) 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子子問題。

第一條特徵是絕大多數問題都可以滿足的,因為問題的計算複雜性一般是隨著問題規模的增加而增加;

第二條特徵是應用分治法的前提它也是大多數問題可以滿足的,此特徵反映了遞迴思想的應用;、

第三條特徵是關鍵,能否利用分治法完全取決於問題是否具有第三條特徵,如果具備了第一條和第二條特徵,而不具備第三條特徵,則可以考慮用貪心法或動態規劃法

第四條特徵涉及到分治法的效率,如果各子問題是不獨立的則分治法要做許多不必要的工作,重複地解公共的子問題,此時雖然可用分治法,但一般用動態規劃法較好


四、分治法的基本步驟

分治法在每一層遞迴上都有三個步驟:

    step1 分解:將原問題分解為若干個規模較小,相互獨立,與原問題形式相同的子問題;

    step2 解決:若子問題規模較小而容易被解決則直接解,否則遞迴地解各個子問題

    step3 合併:將各個子問題的解合併為原問題的解。

它的一般的演算法設計模式如下:

    Divide-and-Conquer(P)

    1. if |P|≤n0

    2. then return(ADHOC(P))

    3. 將P分解為較小的子問題 P1 ,P2 ,...,Pk

    4. for i←1 to k

    5. do yi ← Divide-and-Conquer(Pi) △ 遞迴解決Pi

    6. T ← MERGE(y1,y2,...,yk) △ 合併子問題

    7. return(T)

    其中|P|表示問題P的規模;n0為一閾值,表示當問題P的規模不超過n0時,問題已容易直接解出,不必再繼續分解。ADHOC(P)是該分治法中的基本子演算法,用於直接解小規模的問題P。因此,當P的規模不超過n0時直接用演算法ADHOC(P)求解。演算法MERGE(y1,y2,...,yk)是該分治法中的合併子演算法,用於將P的子問題P1 ,P2 ,...,Pk的相應的解y1,y2,...,yk合併為P的解。


五、分治法的複雜性分析

    一個分治法將規模為n的問題分成k個規模為n/m的子問題去解。設分解閥值n0=1,且adhoc解規模為1的問題耗費1個單位時間。再設將原問題分解為k個子問題以及用merge將k個子問題的解合併為原問題的解需用f(n)個單位時間。用T(n)表示該分治法解規模為|P|=n的問題所需的計算時間,則有:

 T(n)= k T(n/m)+f(n)

    通過迭代法求得方程的解:

    遞迴方程及其解只給出n等於m的方冪時T(n)的值,但是如果認為T(n)足夠平滑,那麼由n等於m的方冪時T(n)的值可以估計T(n)的增長速度。通常假定T(n)是單調上升的,從而當mi≤n<mi+1時,T(mi)≤T(n)<T(mi+1)。 


六、可使用分治法求解的一些經典問題    (1)二分搜尋 (2)大整數乘法  (3)Strassen矩陣乘法 (4)棋盤覆蓋 (5)合併排序 (6)快速排序 (7)線性時間選擇 (8)最接近點對問題 (9)迴圈賽日程表 (10)漢諾塔
七、依據分治法設計程式時的思維過程       實際上就是類似於數學歸納法,找到解決本問題的求解方程公式,然後根據方程公式設計遞迴程式。 1、一定是先找到最小問題規模時的求解方法 2、然後考慮隨著問題規模增大時的求解方法 3、找到求解的遞迴函式式後(各種規模或因子),設計遞迴程式即可。        

二、動態規劃演算法

一、基本概念

    動態規劃過程是:每次決策依賴於當前狀態,又隨即引起狀態的轉移。一個決策序列就是在變化的狀態中產生出來的,所以,這種多階段最優化決策解決問題的過程就稱為動態規劃。

二、基本思想與策略

    基本思想與分治法類似,也是將待求解的問題分解為若干個子問題(階段),按順序求解子階段,前一子問題的解,為後一子問題的求解提供了有用的資訊。在求解任一子問題時,列出各種可能的區域性解,通過決策保留那些有可能達到最優的區域性解,丟棄其他區域性解。依次解決各子問題,最後一個子問題就是初始問題的解。

    由於動態規劃解決的問題多數有重疊子問題這個特點,為減少重複計算,對每一個子問題只解一次,將其不同階段的不同狀態儲存在一個二維陣列中。

    與分治法最大的差別是:適合於用動態規劃法求解的問題,經分解後得到的子問題往往不是互相獨立的(即下一個子階段的求解是建立在上一個子階段的解的基礎上,進行進一步的求解)。

 


 

三、適用的情況

能採用動態規劃求解的問題的一般要具有3個性質:

    (1) 最優化原理:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構,即滿足最優化原理。

    (2) 無後效性:即某階段狀態一旦確定,就不受這個狀態以後決策的影響。也就是說,某狀態以後的過程不會影響以前的狀態,只與當前狀態有關。

   (3)有重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被多次使用到。(該性質並不是動態規劃適用的必要條件,但是如果沒有這條性質,動態規劃演算法同其他演算法相比就不具備優勢)

 


四、求解的基本步驟

     動態規劃所處理的問題是一個多階段決策問題,一般由初始狀態開始,通過對中間階段決策的選擇,達到結束狀態。這些決策形成了一個決策序列,同時確定了完成整個過程的一條活動路線(通常是求最優的活動路線)。如圖所示。動態規劃的設計都有著一定的模式,一般要經歷以下幾個步驟。

    初始狀態→│決策1│→│決策2│→…→│決策n│→結束狀態

                      圖1 動態規劃決策過程示意圖

    (1)劃分階段:按照問題的時間或空間特徵,把問題分為若干個階段。在劃分階段時,注意劃分後的階段一定要是有序的或者是可排序的,否則問題就無法求解。

    (2)確定狀態和狀態變數:將問題發展到各個階段時所處於的各種客觀情況用不同的狀態表示出來。當然,狀態的選擇要滿足無後效性。

    (3)確定決策並寫出狀態轉移方程:因為決策和狀態轉移有著天然的聯絡,狀態轉移就是根據上一階段的狀態和決策來匯出本階段的狀態。所以如果確定了決策,狀態轉移方程也就可寫出。但事實上常常是反過來做,根據相鄰兩個階段的狀態之間的關係來確定決策方法和狀態轉移方程。

    (4)尋找邊界條件:給出的狀態轉移方程是一個遞推式,需要一個遞推的終止條件或邊界條件。

    一般,只要解決問題的階段、狀態和狀態轉移決策確定了,就可以寫出狀態轉移方程(包括邊界條件)。

實際應用中可以按以下幾個簡化的步驟進行設計:

    (1)分析最優解的性質,並刻畫其結構特徵。

    (2)遞迴的定義最優解。

    (3)以自底向上或自頂向下的記憶化方式(備忘錄法)計算出最優值

    (4)根據計算最優值時得到的資訊,構造問題的最優解

 


五、演算法實現的說明

    動態規劃的主要難點在於理論上的設計,也就是上面4個步驟的確定,一旦設計完成,實現部分就會非常簡單。

     使用動態規劃求解問題,最重要的就是確定動態規劃三要素:

    (1)問題的階段 (2)每個階段的狀態

    (3)從前一個階段轉化到後一個階段之間的遞推關係。

     遞推關係必須是從次小的問題開始到較大的問題之間的轉化,從這個角度來說,動態規劃往往可以用遞迴程式來實現,不過因為遞推可以充分利用前面儲存的子問題的解來減少重複計算,所以對於大規模問題來說,有遞迴不可比擬的優勢,這也是動態規劃演算法的核心之處。

    確定了動態規劃的這三要素,整個求解過程就可以用一個最優決策表來描述,最優決策表是一個二維表,其中行表示決策的階段,列表示問題狀態,表格需要填寫的資料一般對應此問題的在某個階段某個狀態下的最優值(如最短路徑,最長公共子序列,最大價值等),填表的過程就是根據遞推關係,從1行1列開始,以行或者列優先的順序,依次填寫表格,最後根據整個表格的資料通過簡單的取捨或者運算求得問題的最優解。

          f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}

 

 


六、動態規劃演算法基本框架 複製程式碼 程式碼 1 for(j=1; j<=m; j=j+1) // 第一個階段
2   xn[j] = 初始值;
3
4  for(i=n-1; i>=1; i=i-1)// 其他n-1個階段
5   for(j=1; j>=f(i); j=j+1)//f(i)與i有關的表示式
6 xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])};
8
9 t = g(x1[j1:j2]); // 由子問題的最優解求解整個問題的最優解的方案
10
11 print(x1[j1]);
12
13 for(i=2; i<=n-1; i=i+1
15 {
17 t = t-xi-1[ji];
18
19 for(j=1; j>=f(i); j=j+1)
21 if(t=xi[ji])
23 break;
25 }
       

三、貪心演算法

一、基本概念:         所謂貪心演算法是指,在對問題求解時,總是做出在 當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的僅是在某種意義上的區域性最優解。       貪心演算法沒有固定的演算法框架,演算法設計的關鍵是貪心策略的選擇。必須注意的是,貪心演算法不是對所有問題都能得到整體最優解,選擇的貪心策略必須具備無後效性,即某個狀態以後的過程不會影響以前的狀態,只與當前狀態有關。      所以對所採用的貪心策略一定要仔細分析其是否滿足無後效性。   二、貪心演算法的基本思路:     1.建立數學模型來描述問題。     2.把求解的問題分成若干個子問題。     3.對每一子問題求解,得到子問題的區域性最優解。     4.把子問題的解區域性最優解合成原來解問題的一個解。   三、貪心演算法適用的問題       貪心策略適用的前提是:區域性最優策略能導致產生全域性最優解。     實際上,貪心演算法適用的情況很少。一般,對一個問題分析是否適用於貪心演算法,可以先選擇該問題下的幾個實際資料進行分析,就可做出判斷。   四、貪心演算法的實現框架     從問題的某一初始解出發;     while (能朝給定總目標前進一步)     {            利用可行的決策,求出可行解的一個解元素;     }     由所有解元素組合成問題的一個可行解;    五、貪心策略的選擇      因為用貪心演算法只能通過解區域性最優解的策略來達到全域性最優解,因此,一定要注意判斷問題是否適合採用貪心演算法策略,找到的解是否一定是問題的最優解。   六、例題分析     下面是一個可以試用貪心演算法解的題目,貪心解的確不錯,可惜不是最優解。     [揹包問題]有一個揹包,揹包容量是M=150。有7個物品,物品可以分割成任意大小。     要求儘可能讓裝入揹包中的物品總價值最大,但不能超過總容量。     物品 A B C D E F G     重量 35 30 60 50 40 10 25     價值 10 40 30 50 35 40 30     分析:     目標函式: ∑pi最大     約束條件是裝入的物品總重量不超過揹包容量:∑wi<=M( M=150)     (1)根據貪心的策略,每次挑選價值最大的物品裝入揹包,得到的結果是否最優?     (2)每次挑選所佔重量最小的物品裝入是否能得到最優解?     (3)每次選取單位重量價值最大的物品,成為解本題的策略。     值得注意的是,貪心演算法並不是完全不可以使用,貪心策略一旦經過證明成立後,它就是一種高效的演算法。     貪心演算法還是很常見的演算法之一,這是由於它簡單易行,構造貪心策略不是很困難。     可惜的是,它需要證明後才能真正運用到題目的演算法中。     一般來說, 貪心演算法的證明圍繞著:整個問題的最優解一定由在貪心策略中存在的子問題的最優解得來的。     對於例題中的3種貪心策略,都是無法成立(無法被證明)的,解釋如下:     (1)貪心策略:選取價值最大者。反例:     W=30     物品:A B C     重量:28 12 12     價值:30 20 20     根據策略,首先選取物品A,接下來就無法再選取了,可是,選取B、C則更好。     (2)貪心策略:選取重量最小。它的反例與第一種策略的反例差不多。     (3)貪心策略:選取單位重量價值最大的物品。反例:     W=30     物品:A B C     重量:28 20 10     價值:28 20 10     根據策略,三種物品單位重量價值一樣,程式無法依據現有策略作出判斷,如果選擇A,則答案錯誤。    

四、回溯法

 

1、概念

      回溯演算法實際上一個類似列舉的搜尋嘗試過程,主要是在搜尋嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。

   回溯法是一種選優搜尋法,按選優條件向前搜尋,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法,而滿足回溯條件的某個狀態的點稱為“回溯點”。

     許多複雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。

2、基本思想

   在包含問題的所有解的解空間樹中,按照深度優先搜尋的策略,從根結點出發深度探索解空間樹。當探索到某一結點時,要先判斷該結點是否包含問題的解,如果包含,就從該結點出發繼續探索下去,如果該結點不包含問題的解,則逐層向其祖先結點回溯。(其實回溯法就是對隱式圖的深度優先搜尋演算法)。

       若用回溯法求問題的所有解時,要回溯到根,且根結點的所有可行的子樹都要已被搜尋遍才結束。

       而若使用回溯法求任一個解時,只要搜尋到問題的一個解就可以結束。

3、用回溯法解題的一般步驟:

    (1)針對所給問題,確定問題的解空間:

            首先應明確定義問題的解空間,問題的解空間應至少包含問題的一個(最優)解。

    (2)確定結點的擴充套件搜尋規則

    (3)以深度優先方式搜尋解空間,並在搜尋過程中用剪枝函式避免無效搜尋。

4、演算法框架

     (1)問題框架

      設問題的解是一個n維向量(a1,a2,………,an),約束條件是ai(i=1,2,3,…..,n)之間滿足某種條件,記為f(ai)。

     (2)非遞歸回溯框架

 1: int a[n],i;
   2: 初始化陣列a[];
   3: i = 1;
   4: while (i>0(有路可走)   and  (未達到目標))  // 還未回溯到頭
   5: {
   6:     if(i > n)                                              // 搜尋到葉結點
   7:     {   
   8:           搜尋到一個解,輸出;
   9:     }
  10:     else                                                   // 處理第i個元素
  11:     { 
  12:           a[i]第一個可能的值;
  13:           while(a[i]在不滿足約束條件且在搜尋空間內)
  14:           {
  15:               a[i]下一個可能的值;
  16:           }
  17:           if(a[i]在搜尋空間內)
  18:          {
  19:               標識佔用的資源;
  20:               i = i+1;                              // 擴充套件下一個結點
  21:          }
  22:          else 
  23:         {
  24:               清理所佔的狀態空間;            // 回溯
  25:               i = i –1; 
  26:          }
  27: }

3)遞迴的演算法框架

         回溯法是對解空間的深度優先搜尋,在一般情況下使用遞迴函式來實現回溯法比較簡單,其中i為搜尋的深度,框架如下:

1: int a[n];
   2: try(int i)
   3: {
   4:     if(i>n)
   5:        輸出結果;
   6:      else
   7:     {
   8:        for(j = 下界; j <= 上界; j=j+1)  // 列舉i所有可能的路徑
   9:        {
  10:            if(fun(j))                 // 滿足限界函式和約束條件
  11:              {
  12:                 a[i] = j;
  13:               ...                         // 其他操作
  14:                 try(i+1);
  15:               回溯前的清理工作(如a[i]置空值等);
  16:               }
  17:          }
  18:      }
  19: }


 

五、分支限界法

 

一、基本描述

    類似於回溯法,也是一種在問題的解空間樹T上搜索問題解的演算法。但在一般情況下,分支限界法與回溯法的求解目標不同。回溯法的求解目標是找出T中滿足約束條件的所有解,而分支限界法的求解目標則是找出滿足約束條件的一個解,或是在滿足約束條件的解中找出使某一目標函式值達到極大或極小的解,即在某種意義下的最優解

   (1)分支搜尋演算法

    所謂“分支”就是採用廣度優先的策略,依次搜尋E-結點的所有分支,也就是所有相鄰結點,拋棄不滿足約束條件的結點,其餘結點加入活結點表。然後從表中選擇一個結點作為下一個E-結點,繼續搜尋。

     選擇下一個E-結點的方式不同,則會有幾種不同的分支搜尋方式。

   1)FIFO搜尋

   2)LIFO搜尋

   3)優先佇列式搜尋

(2)分支限界搜尋演算法 

二、分支限界法的一般過程

    由於求解目標不同,導致分支限界法與回溯法在解空間樹T上的搜尋方式也不相同。回溯法以深度優先的方式搜尋解空間樹T,而分支限界法則以廣度優先或以最小耗費優先的方式搜尋解空間樹T

    分支限界法的搜尋策略是:在擴充套件結點處,先生成其所有的兒子結點(分支),然後再從當前的活結點表中選擇下一個擴充套件對點。為了有效地選擇下一擴充套件結點,以加速搜尋的程序,在每一活結點處,計算一個函式值(限界),並根據這些已計算出的函式值,從當前活結點表中選擇一個最有利的結點作為擴充套件結點,使搜尋朝著解空間樹上有最優解的分支推進,以便儘快地找出一個最優解。

    分支限界法常以廣度優先或以最小耗費(最大效益)優先的方式搜尋問題的解空間樹。問題的解空間樹是表示問題解空間的一棵有序樹,常見的有子集樹和排列樹。在搜尋問題的解空間樹時,分支限界法與回溯法對當前擴充套件結點所使用的擴充套件方式不同。在分支限界法中,每一個活結點只有一次機會成為擴充套件結點。活結點一旦成為擴充套件結點,就一次性產生其所有兒子結點。在這些兒子結點中,那些導致不可行解或導致非最優解的兒子結點被捨棄,其餘兒子結點被子加入活結點表中。此後,從活結點表中取下一結點成為當前擴充套件結點,並重覆上述結點擴充套件過程。這個過程一直持續到找到所求的解或活結點表為空時為止。

三、回溯法和分支限界法的一些區別

    有一些問題其實無論用回溯法還是分支限界法都可以得到很好的解決,但是另外一些則不然。也許我們需要具體一些的分析——到底何時使用分支限界而何時使用回溯呢?

回溯法和分支限界法的一些區別:

   方法對解空間樹的搜尋方式       儲存結點的常用資料結構      結點儲存特性常用應用

  回溯法深度優先搜尋堆疊活結點的所有可行子結點被遍歷後才被從棧中彈出找出滿足約束條件的所有解

  分支限界法廣度優先或最小消耗優先搜尋隊列、優先佇列每個結點只有一次成為活結點的機會找出滿足約束條件的一個解或特定意義下的最優解