遞迴回溯相關

suprefan發表於2020-11-02

遞迴回溯問題及DFS/BFS相關總結

一、遞迴

1. 何為遞迴?
遞迴,即函式(方法)自己呼叫自己,亦或者稱為套娃。

2. 遞迴的簡單模板
遞迴不能無限的進行,因為遞迴的原理跟棧有關,如果無限的呼叫遞迴就會棧溢位,所以必須在一開始就設定遞迴的終止條件。如下面這樣

public void recur(引數 0 ){
    if 終止條件
        return;
    recur(引數 1);
}

但是不能寫成下面這樣

public void recur(引數 0 ){
    recur(引數 1);
    if 終止條件
        return;
}

如果是這樣寫就會無限的遞迴下去,最後StackOverflow

3. 遞迴的簡單例子

3.1 階乘

public int factorial(int n){
   if(n <= 1)
       return 1;
   else
       return n * factorial (n - 1);         
}

3.2 斐波那契數列

public int Fibonacci(int n){
   if(n == 0)
       return 0;
   if(n == 1)
       return 1;
   else
       return Fibonacci(n - 1) + Fibonacci(n - 2);
}

3.3 漢諾塔

public void hanoi(int n, char x, char y, char z){
   if(n == 1){
       System.out.printf("%c -> %c\n",x, z);  
   } else {
       hanoi(n - 1,x, z, y);
       System.out.printf("%c -> %c\n",x, z);  
       hanoi(n - 1,y, x, z);
   }
}

4. 適合用遞迴的場合
    如果一個問題可以拆分成更小的子問題,且子問題和原問題有相同的結構,就可以嘗試用遞迴去寫。但是要考慮空間的限制

5. 如何更好的理解遞迴
    一開始接觸遞迴總歸會有點懵,因為原來寫的程式碼都是顯式的,你可以跟著程式碼理順下來,或者一步步debug,但是遞迴是隱式的(藉助棧)。
    以階乘為例來輔助理解,加入要求4的階乘,用上面的程式會是怎樣的流程呢?

f(4)
->4 * f(3)
->4 * (3 * f(2))
->4 * (3 * (2 * f(1)))
->4 * (3 * (2 * 1))
->4 * (3 * 2)
->4 * 6
->24

前面4行表示的是因為沒有觸發終止條件一直向下拆分成更小的子問題的過程,後面的表示觸發終止條件後向上回彈代入的過程。遞迴就是一個先遞進再回歸的過程。如果想更好的理解遞迴,我的辦法是從遞迴的終止條件向上一級一級的推導,可能會好理解一點。

二、回溯

1. 什麼叫回溯演算法
    簡單點來說回溯演算法是一種解決問題的思想,它通常要通過遞迴來實現,所以放在一起復習,而且確實一開始很不好理解,需要多琢磨。

    回溯演算法的思路就是暴力求解,不停探索的解決問題思路。從起點出發,先朝著一個方向走,直到走不通的時候再回退一步重新做選擇。這種走不通就退回再走的思路成為回溯法。結合生活的一個例子來說,猜一個一位數的密碼,第一位先猜0,不對就再猜別的1-9,知道猜對。這種不停嘗試的列舉法就是回溯。

2. 回溯演算法例項
    最近做題經常碰到回溯相關的題目,後面的DFS也跟回溯聯絡緊密,就剛好整理複習一下。

力扣78.子集
問題描述:給定一組不含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)。
說明:解集不能包含重複的子集。
示例:
輸入: nums = [1,2,3]
輸出:
[
    [1],
    [1,2],
    [1,2,3],
    [1,3],
    [2],
    [2,3],
    [3],
    []
]

class solution{
      //定義全域性變數的話就不用了給函式傳參了
      //視個人習慣而定
      List<List<Integer>> res = new ArrayList<>();
      List<Integer> list = new ArrayList<>();
      public List<List<Integer>> subsets(int[] nums){
          dfs(nums, 0);
          return res;
      }
      /**
        *param nums:傳入的陣列
        *param index:可以理解為層數
        */
      public void dfs(int[] nums, int index){
          res.add(new ArrayList<>(list));
          for(int i = index; i < nums.length; i++){
              list.add(nums[i]);
              dfs(nums, i + 1);
              //避免分支汙染
              list.remove(list.size() - 1);                
          }
      }
}

力扣46.全排列
問題描述:給定一個沒有重複數字的序列,返回其所有可能的全排列。
示例:
輸入: nums = [1,2,3]
輸出:
[
    [1,2,3],
    [1,3,2],
    [2,1,3],
    [2,3,1],
    [3,1,2],
    [3,2,1],
]

class solution{
      List<List<Integer>> res = new ArrayList<>();
      public List<List<Integer>> permute(int[] nums){
          dfs(nums,new ArrayList<>());
          return res;
      }
      public void dfs(int[] nums, List<Integer> list){
          if(list.size() == nums.length)
              res.add(new ArrayList<>(list));
          for(int i = 0; i < nums.length; i++){
              if(!list.contains(nums[i])){
                  list.add(nums[i]);
                  dfs(nums, list);
                  list.remove(list.size() - 1);                 
              }
          }
      }
}

力扣39.組合總數
問題描述:給定一個無重複元素的陣列candidates和一個目標數target,找出candidates中所有可以使數字和為target的組合。
candidates中的數字可以無限制重複被選取。
示例:
輸入: candidates = [2,3,6,7],  target = 7,
所求解集:
[
    [7],
    [2,2,3],
]

class solution{
      List<List<Integer>> res = new ArrayList<>();
      public List<List<Integer>> combinationSum(int[] candidates, int target){
          dfs(candidates, target, new ArrayList<>());
          return res;
      }
      public void dfs(int[] nums, int target, List<Integer> list, int index){
          if(target == 0)
              res.add(new ArrayList<>(list));
          for(int i = index; i < nums.length; i++){
              if(nums[i] > target)
                  continue;    
              list.add(nums[i]);
              dfs(nums,target - nums[i],list,i);
              list.remove(list.size() - 1);
          }
      }
}

相關文章