算法漫游指北(第十篇):泛型遞歸、遞歸代碼模板、遞歸思維要點、分治算法、回溯算法


一、泛型遞歸

遞歸 Recursion:

  • 又譯為遞回,在數學與計算機科學中,是指在函數的定義中使用函數自身的方法。

  • 遞歸一詞還較為常用於描述以自相似方法重復事物的過程。

  • 在數學和計算機科學中,遞歸指由一種(或多種)簡單的基本情況定義的一類對象或方法,並規定其他所有情況都能被還原為其基本情況。

斐波那契數列是典型的遞歸案例,遞歸 是一種特殊的 循環,通過函數體來進行的循環

示例:

# 計算 n!
n! = 1 * 2 * 3 * ... * n
​
def Factorial(n):
    if n <=1:
        return 1
    return n * Factorial(n - 1)

  

遞歸代碼模板(重點記下來)

def recursion(level,param1,param2,...):
    # recursion terminator  1.遞歸終結者,遞歸中止的條件
    if level > MAX_LEVEL:
        process_result
        return
​
    # process logic in current level  2.處理當前層邏輯
    process(level,data...)
​
    # drill down  3.下探到下一層
    self.recursion(level + 1,p1,...)
​
    #reverse the current level status if needed  4.清理當前層(比如需要清理的全局變量),不是必選項

  

  1. 遞歸終止條件(terminator):寫遞歸函數開始的話,一定要先把函數遞歸終止條件寫上,否則,會導致無限遞歸或者死循環。

  2. 處理當前層的邏輯(current level logic):完成這一層要進行的業務代碼、邏輯代碼

  3. 下探到下一層(drill down):這里的參數來標記當前是哪一層,這里level必須加1,同時把相應的參數p1、p2、p3...放下去就行了。

  4. 清理(reverse):遞歸完了,有些東西可能要清理,(非必需)

遞歸思維要點

  1. 不要人肉進行遞歸(最大誤區),初學者可以在紙上畫出遞歸的狀態樹,慢慢熟練之后一定要拋棄這樣的習慣。一定要記住:直接看函數本身開始寫即可。否則,永遠沒辦法掌握、熟練使用遞歸。

  2. 找到最近最簡的方法,將其拆解成可重復解決的問題(找最近重復子問題)。原因是我們寫的程序的指令,只包括 if else 、 for 和 while loop、遞歸調用。

  3. 數學歸納法的思維,最開始最簡單的條件是成立的,比如n=1,n=2的時候是成立的,且第二點你能證明當n成立的時候,可以推導出n+1也成立的。

 

 

二、分治算法(Divide & Conquer)

分治和回溯,本質上就是遞歸,是遞歸的一個細分類。可以理解為分治和回溯,就是一種特殊的遞歸,或者是較為復雜的遞歸。

本質上就是找重復性以及分解問題,和最后組合每個子問題的結果,都是這一個思路。

碰到一個題目,就會找到他的重復性:

  1. 最優重復性:動態規划

  2. 最近重復性:根據重復性的構造和分解,便有分治和回溯。

 

 

 

分治的思想:本質是遞歸,在遞歸狀態樹的時候,對一個問題會化解成好幾個子問題。

基本概念

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

遞歸狀態樹

 

基本思想及策略

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

 

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

分治法適用的情況

分治法所能解決的問題一般具有以下幾個特征: 1) 該問題的規模縮小到一定的程度就可以容易地解決 2) 該問題可以分解為若干個規模較小的相同問題,即該問題具有最優子結構性質。 3) 利用該問題分解出的子問題的解可以合並為該問題的解; 4) 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子子問題。 第一條特征是絕大多數問題都可以滿足的,因為問題的計算復雜性一般是隨着問題規模的增加而增加; 第二條特征是應用分治法的前提它也是大多數問題可以滿足的,此特征反映了遞歸思想的應用;、 第三條特征是關鍵,能否利用分治法完全取決於問題是否具有第三條特征,如果具備了第一條和第二條特征,而不具備第三條特征,則可以考慮用貪心法或動態規划法。 第四條特征涉及到分治法的效率,如果各子問題是不獨立的則分治法要做許多不必要的工作,重復地解公共的子問題,此時雖然可用分治法,但一般用動態規划法較好。

分治法的基本步驟

分治算法就是將原問題划分成n個規模較小,並且結構與原問題相似的子問題,遞歸地解決這些子問題,然后再合並其結果,就得到原問題的解。

分治算法的遞歸實現中,每一層遞歸都會涉及這樣三個操作:

  • 分解:將原問題分解成一系列子問題

  • 解決:遞歸地求解各個子問題,若子問題足夠小,則直接求解

  • 合並:將子問題的結果合並成原問題

 

分治算法能解決的問題,一般需要滿足下面這幾個條件:

  • 原問題與分解成的小問題具有相同的模式

  • 原問題分解成的子問題可以獨立求解,子問題之間沒有相關性

  • 具有分解終止條件,也就是說,當問題足夠小,可以直接求解

  • 可以將子問題合並成原問題,而這個合並操作的復雜度不能太高,否則就起不到減小算法總體復雜度的效果了

分治代碼模板

def divide_conquer(problem, param1, param2, ...):
    # recursion terminator 終止條件
    if problem is None:
        print_result
        return
    # prepare data  處理當前層邏輯
    data = prepare_data(problem)
    subproblems = split_problem(problem, data)
    # conquer subproblems  調用函數下探到下一層,解決更細節的子問題
    subresult1 = self.divide_conquer(subproblems[0], p1, ...)
    subresult2 = self.divide_conquer(subproblems[1], p1, ...)
    subresult3 = self.divide_conquer(subproblems[2], p1, ...)
    ...
    # process and generate the final result
    result = process_result(subresult1, subresult2, subresult3, ...)
​
    # revert the current level states
    # 與泛型遞歸不同就是在drill down與revert state中間加了一步
    # ---> 就是把drill down得到的子結果要合並在一起,返回回去。

  

 

 

三、回溯算法(Backtracking)

回溯基本概念

回溯的處理思想有點類似枚舉搜索。通過枚舉所有的解,找到滿足期望的解。為了有規律地枚舉所有可能的解,避免遺漏和重復,我們把問題求解的過程分為多個階段。每個階段,都會面對一個岔路口,我們先隨意選一條路走,當發現這條路走不通的時候(不符合期望的解),就回退到上一個岔路口,另選一種走法繼續走。

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

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

 

回溯可以理解為遞歸的一種問題,不斷地在每一層去試,每一層有不同的辦法,類似於一個一個去試,看這個方法是否可行。最典型的應用是八皇后問題、數獨

 

回溯基本思想

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

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

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

 

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

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

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

(2)確定結點的擴展搜索規則

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

 

回溯法采用試錯的思想,它嘗試分布的去解決一個問題。在分步解決問題的過程中,當它通過嘗試發現現有的分步答案不能得到有效的正確的解答的時候,它將取消上一步甚至是上幾步的計算,再通過其他的可能的分步解答再次嘗試尋找問題的答案。

回溯法通常用最簡單的遞歸方法來實現,在反復重試上述的步驟后可能出現兩種情況:

  • 找到一個可能存在的正確的答案;

  • 在嘗試了所有可能的分步方法后宣告該問題沒有答案。 在最壞的情況下,回溯法會導致一次復雜度為指數時間的計算。

 

 

 

 

參考資料

 

[1]https://blog.csdn.net/qq_40378034/java/article/details/102764545

[2]https://www.cnblogs.com/yddzyy/p/5587643.html


免責聲明!

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



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