徹底理解回溯法的精要
目錄
- 問題分析
- 使用什麼方法?
- 什麼是回溯法?
- 怎麼使用回溯法?
- 回溯法的具體實施
- 回溯法的延伸
給定一個沒有重複數字的序列,返回其所有可能的全排列。
示例:
輸入: [1,2,3]
輸出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
問題分析
使用什麼方法?
全排列很明顯使用回溯法來進行解答
什麼是回溯法?
回溯法(探索與回溯法)是一種選優搜尋法,又稱為試探法,按選優條件向前搜尋,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法,而滿足回溯條件的某個狀態的點稱為“回溯點”。
怎麼使用回溯法?
運用回溯法解題的關鍵要素有以下三點:
- 針對給定的問題,定義問題的解空間;
- 確定易於搜尋的解空間結構;
以深度優先方式搜尋解空間,並且在搜尋過程中用剪枝函式避免無效搜尋。
什麼是深度優先搜尋?
深度優先搜尋(縮寫DFS)有點類似廣度優先搜尋,也是對一個連通圖進行遍歷的演算法。它的思想是從一個頂點V0開始,沿著一條路一直走到底,如果發現不能到達目標解,那就返回到上一個節點,然後從另一條路開始走到底,這種儘量往深處走的概念即是深度優先的概念。
程式碼模板是什麼樣子的?
void BackTrace(int t) { if(t>n) Output(x); else for(int i = f (n, t); i <= g (n, t); i++ ) { x[t] = h(i); if(Constraint(t) && Bound (t)) BackTrace(t+1); } }
其中,t
表示遞迴深度,即當前擴充套件結點在解空間樹中的深度;n
用來控制遞迴深度,即解空間樹的高度。當t>n
時,演算法已搜尋到一個葉子結點,此時由函式Output(x)
對得到的可行解x
進行記錄或輸出處理
用 f(n, t)
和 g(n, t)
分別表示在當前擴充套件結點處未搜尋過的子樹的起始編號和終止編號;h(i)
表示在當前擴充套件結點處x[t]
的第i
個可選值;函式Constraint(t)
和 Bound(t)
分別表示當前擴充套件結點處的約束函式和限界函式。若函式Constraint(t)
的返回值為真,則表示當前擴充套件結點處x[1:t]
的取值滿足問題的約束條件;否則不滿足問題的約束條件。若函式Bound(t)
x[1:t]
的取值尚未使目標函式越界,還需由BackTrace(t+1)
對其相應的子樹做進一步地搜尋;否則,在當前擴充套件結點處x[1:t]
的取值已使目標函式越界,可剪去相應的子樹。
回溯法的具體實施
class Solution {
public List<List<Integer>> permute(int[] nums) {
//LeetCode程式碼模板
}
}
step 1 定義問題的解空間
什麼是解空間?
應用回溯法求解問題時,首先應明確定義問題的解空間,該解空間應至少包含問題的一個最優解。例如,對於有
n
種物品的0-1
揹包問題,其解空間由長度為n
的0-1
向量組成,該解空間包含了對變數的所有可能的0-1
賦值。當n=3
時,其解空間是{ (0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1) }
在定義了問題的解空間後,還需要將解空間有效地組織起來,使得回溯法能方便地搜尋整個解空間,通常將解空間組織成樹或圖的形式。例如,對於n= 3
的0-1
揹包問題,其解空間可以用一棵完全二叉樹表示,從樹根到葉子結點的任意一條路徑可表示解空間中的一個元素,如從根結點A
到結點J
的路徑對應於解空間中的一個元素(1, 0, 1)
。
定義本題的解空間
全排列問題,因為輸入陣列的長度為n = nums.length
,解空間就是一個森林:
這裡需要一個森林的圖
假設n=4
且nums[]={1,2,3,4}
則解空間應該是
第一層:1 2 3 4
第二層:12 13 14 /21 23 24/31 32 34
第三層:123 124/132 134/213 214/231 234/241 243/312 314/.....
第四層:略
確定易於搜尋的解空間結構
解空間主要對應的是子集樹和排列樹,依據題意進行選擇。(根據題意畫個圖,就知道了)
什麼是子集樹???
子集樹是一個數學學科詞彙,屬於函式類,當所給問題是從
n
個元素的集合S
中找出S
滿足某種性質的子集時,相應的解空間稱為子集樹。
當所給問題是從n
個元素的集合S
中找出S
滿足某種性質的子集時,相應的解空間稱為子集樹。例如:n
個物品的0-1
揹包問題所相應的解空間是一棵子集樹,這類子集樹通常有2^n
個葉結點,其結點總數為(2^(n+1))-1
。遍歷子集樹的演算法通常需O(2^n)
計算時間。
什麼是排列樹??
當所給問題是確定
n
個元素滿足某種性質的排列時,相應的解空間樹稱為排列樹。排列樹通常有n!
個葉子節點。因此遍歷排列樹需要O(n!)
的計算時間。
上面已經確定,要將解空間構建成子集樹
的形式
step 2 回溯法的精髓
回溯的精髓
退回原狀態
如何回退是回溯的精髓,什麼時候回退
就本題而言,第一躺全排列應該是1->2->3->4
,當走到最後一步4
之後,應該回退一步到1->2->3
因為3
只有一個分支4
,再回退一步到1->2
,然後滿足了約束函式可以進行下一步1->2->4
;
對於本題,回退到方法在於,標記未被訪問的陣列下標,回退則重製標記
因此可以使用一個visited[]
陣列,陣列的長度為nums.length
,被訪問則對應的下標標記為true
,否則標記為false
;
step 3 回溯函式的設計
void BackTrace(int t)
只傳遞一個引數的話顯然是無法滿足本題的,因為本題包含了一下5個需要傳遞的引數:
visited[]
陣列;t
遞迴深度;List<List<Integer>> output
儲存所有解的大容器List<Integer> save
儲存解的小容器nums[]
原始資料
因此,BackTrace
應設計為:
public static void BackTrace( List<Integer> save, List<List<Integer>> out, boolean visited[], int nums[]) {
if (save.size() == nums.length) {
out.add(new ArrayList<>(save));
return;
} else
for (int i = 0; i < nums.length; i++) {
if (visited[i]) continue;
visited[i] = true;
save.add(nums[i]);
BackTrace( save, out, visited, nums);
save.remove(save.size() - 1);
visited[i] = false;
}
}
怎麼寫出這段程式碼需要結合前面的內容反覆的思考 =-= 我想了好久才理清楚回溯的思路
回溯法的延伸
子集問題
題目:
給定一組不含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)。
說明:解集不能包含重複的子集。
示例:
輸入: nums = [1,2,3] 輸出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
從上題中我們可以得出結論,這仍然是一道需要使用回溯法的題目。
解空間與解空間結構
很明顯這是一個子集數的解空間結構
假設
n=3
且nums[]={1,2,3}
則解空間應該是
第一層:1 2 3
第二層:12 13/21 23/31 32
第三層:123 132/213 231/312 321/
關鍵性問題
- 通過什麼方法回退?
- 約束條件是什麼?
- 去除重複物件
檢測重複
檢測重複首先想到的會是雜湊表HashMap.因此每一次新增都應該在新增之前查詢,如果找到重複則不存入;
約束條件是什麼
約束條件應該還是當遍歷到最後一個元素時退出?
通過什麼方法回退?
由於集合的特殊性。不需要回退;
函式的設計:
public static void BackTrack(int t,int[] nums, List<List<Integer>> out, List<Integer> save) {
out.add(new ArrayList<>(save));
for (int i = t; i < nums.length; i++) {
save.add(nums[i]);
BackTrack(i+1,nums, out, save);
save.remove(save.size()-1);
}
}