給定一個沒有重複數字的序列,返回其全部可能的全排列。
示例:算法
輸入: [1,2,3] 輸出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
全排列很明顯使用回溯法來進行解答數組
回溯法(探索與回溯法)是一種選優搜索法,又稱爲試探法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步從新選擇,這種走不通就退回再走的技術爲回溯法,而知足回溯條件的某個狀態的點稱爲「回溯點」。函數
運用回溯法解題的關鍵要素有如下三點:設計
以深度優先方式搜索解空間,而且在搜索過程當中用剪枝函數避免無效搜索。code
深度優先搜索(縮寫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]
的取值已使目標函數越界,可剪去相應的子樹。rem
class Solution { public List<List<Integer>> permute(int[] nums) { //LeetCode代碼模板 } }
應用回溯法求解問題時,首先應明肯定義問題的解空間,該解空間應至少包含問題的一個最優解。例如,對於有
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!)
的計算時間。
上面已經肯定,要將解空間構建成子集樹
的形式
退回原狀態
如何回退是回溯的精髓,何時回退
就本題而言,第一躺全排列應該是1->2->3->4
,當走到最後一步4
以後,應該回退一步到1->2->3
由於3
只有一個分支4
,再回退一步到1->2
,而後知足了約束函數能夠進行下一步1->2->4
;
對於本題,回退到方法在於,標記未被訪問的數組下標,回退則重製標記
所以能夠使用一個visited[]
數組,數組的長度爲nums.length
,被訪問則對應的下標標記爲true
,不然標記爲false
;
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); } }