給定一個沒有重復數字的序列,返回其所有可能的全排列。
示例:
輸入: [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,返回該數組所有可能的子集(冪集)。
說明:解集不能包含重復的子集。
示例:
輸出:
[
[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);
}
}