遞歸與回溯的理解


LeetCode 刷題筆記——遞歸與回溯的理解

回溯算法詳解

遞歸

思路

程序調用自身的編程技巧稱為遞歸( recursion)。 
遞歸做為一種算法在程序設計語言中廣泛應用。 一個過程或函數在其定義或說明中有直接或間接調用自身的一種方法,它通常把一個大型復雜的問題層層轉化為一個與原問題相似的規模
較小的問題來求解,遞歸策略只需少量的程序就可描述出解題過程所需要的多次重復計算,大大地減少了程序的代碼量。

通常來說,為了描述問題的某一狀態,必須用到該狀態的上一個狀態;而如果要描述上一個狀態,又必須用到上一個狀態的上一個狀態…… 這樣用自己來定義自己的方法就是遞歸。

寫遞歸心得

明白一個函數的作用並相信它能完成這個任務,千萬不要試圖跳進細節。千萬不要跳進這個函數里面企圖探究更多細節,否則就會陷入無窮的細節無法自拔,人腦能壓幾個棧啊。

以Path sum 3為例

給一課二叉樹,和一個目標值,節點上的值有正有負,返回樹中和等於目標值的路徑條數,讓你編寫 pathSum 函數:

root = [10,5,-3,3,2,null,11,3,-2,null,1],
sum = 8

      10
     /  \
    5   -3
   / \    \
  3   2   11
 / \   \
3  -2   1

Return 3. The paths that sum to 8 are:

1.  5 -> 3
2.  5 -> 2 -> 1
3. -3 -> 11

------題解

//給他一個節點和一個目標值,他返回以這個節點為根的樹中,和為目標值的路徑總數。
int pathSum(TreeNode root, int sum) {
    if (root == null) return 0;
    int pathImLeading = count(root, sum); // 自己為開頭的路徑數
    int leftPathSum = pathSum(root.left, sum); // 左邊路徑總數(相信他能算出來)
    int rightPathSum = pathSum(root.right, sum); // 右邊路徑總數(相信他能算出來)
    return leftPathSum + rightPathSum + pathImLeading;
}

//給他一個節點和一個目標值,他返回以這個節點為根的樹中,能湊出幾個以該節點為路徑開頭,和為目標值的路徑總數。
int count(TreeNode node, int sum) {
    if (node == null) return 0;
    // 我自己能不能獨當一面,作為一條單獨的路徑呢?
    int isMe = (node.val == sum) ? 1 : 0;
    // 左邊的小老弟,你那邊能湊幾個 sum - node.val 呀?
    int leftBrother = count(node.left, sum - node.val); 
    // 右邊的小老弟,你那邊能湊幾個 sum - node.val 呀?
    int rightBrother = count(node.right, sum - node.val);
    return  isMe + leftBrother + rightBrother; // 我這能湊這么多個
}

------與之前解法相比

與之前解法相比,會慢點,但是思路值得學習。

[LeetCode] 437. 路徑總和 III ☆☆☆(遞歸)

回溯

思路

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

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

回溯的思路基本如下:當前局面下,我們有若干種選擇,所以我們對每一種選擇進行嘗試。如果發現某種選擇違反了某些限定條件,此時 return;如果嘗試某種選擇到了最后,發現該選擇是正確解,那么就將其加入到解集中。
在這種思想下,我們需要清晰的找出三個要素:選擇 (Options),限制 (Restraints),結束條件 (Termination)。

回溯算法框架

result = []
def backtrack(路徑, 選擇列表):
    if 滿足結束條件:
        result.add(路徑)
        return
    
    for 選擇 in 選擇列表:
        做選擇
        backtrack(路徑, 選擇列表)
        撤銷選擇

其核心就是 for 循環里面的遞歸,在遞歸調用之前「做選擇」,在遞歸調用之后「撤銷選擇」,特別簡單。

以全排列為例

List<List<Integer>> res = new LinkedList<>();

/* 主函數,輸入一組不重復的數字,返回它們的全排列 */
List<List<Integer>> permute(int[] nums) {
    // 記錄「路徑」
    LinkedList<Integer> track = new LinkedList<>();
    backtrack(nums, track);
    return res;
}

// 路徑:記錄在 track 中
// 選擇列表:nums 中不存在於 track 的那些元素
// 結束條件:nums 中的元素全都在 track 中出現
void backtrack(int[] nums, LinkedList<Integer> track) {
    // 觸發結束條件
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的選擇
        if (track.contains(nums[i]))
            continue;
        // 做選擇
        track.add(nums[i]);
        // 進入下一層決策樹
        backtrack(nums, track);
        // 取消選擇
        track.removeLast();
    }
}

 

遞歸與回溯的區別

遞歸是一種算法結構。遞歸會出現在子程序中,形式上表現為直接或間接的自己調用自己。典型的例子是階乘,計算規律為:n!=n×(n1)!

回溯是一種算法思想,它是用遞歸實現的。回溯的過程類似於窮舉法,但回溯有“剪枝”功能,即自我判斷過程。例如有求和問題,給定有 7 個元素的組合 [1, 2, 3, 4, 5, 6, 7],求加和為 7 的子集。累加計算中,選擇 1+2+3+4 時,判斷得到結果為 10 大於 7,那么后面的 5, 6, 7 就沒有必要計算了。這種方法屬於搜索過程中的優化,即“剪枝”功能。

 

用一個比較通俗的說法來解釋遞歸和回溯:
我們在路上走着,前面是一個多岔路口,因為我們並不知道應該走哪條路,所以我們需要嘗試。嘗試的過程就是一個函數。
我們選擇了一個方向,后來發現又有一個多岔路口,這時候又需要進行一次選擇。所以我們需要在上一次嘗試結果的基礎上,再做一次嘗試,即在函數內部再調用一次函數,這就是遞歸的過程。
這樣重復了若干次之后,發現這次選擇的這條路走不通,這時候我們知道我們上一個路口選錯了,所以我們要回到上一個路口重新選擇其他路,這就是回溯的思想。

 


免責聲明!

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



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