回溯法是學習演算法的乙個坎,它將許多**基礎不夠紮實的同學擋在通往更高水平的門外。究其原因,是因為回溯法本身與遞迴有著千絲萬縷的關係,同時也要求學習者具備較強的抽象問題的能力。所以在學習回溯法之前,學習者必須熟練解決一些常見的遞迴問題,如二叉樹的遍歷、lca問題等,這些問題leetcode均有題目,讀者不妨先去練練手再回來。如果能熟練寫出常見遞迴問題的**,回溯法也就如庖丁解牛了。下面介紹兩個回溯法練習的入門題,陣列元素的全排列和全組合求解。
leetcode第46題考察了這個問題。問題很簡單,高中生也懂,如下:
給定乙個 沒有重複 數字的序列,返回其所有可能的全排列。如果手算這個問題的答案,我們是如何計算呢?示例:輸入: [1,2,3]
輸出:[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
(1)選取排列的第乙個數字,可選1、2、3,按序從陣列中拿出1,
(2)選取排列的第二個數字,由於1已經被選了,可選2、3,先選2,
(3)選取排列的第三個數字,由於1、2被選,只能選3,得到第乙個排列[1,2,3]。
注意在第二步的時候,我們可選2、3,但是我們已經選了2;所以在第三步結束時我們退回第二步,再選3,然後進入第三步,這時可選2,這樣就得到了[1、3、2]。最終退回第一步重複即可得到所有的排列。
在上述的敘述中,有乙個退回的步驟,這就是我們所說的回溯。它是用遞迴實現的,相當於使用了乙個棧儲存了"案發現場",等退回上一次遞迴時,程式繼續執行。有興趣的讀者可以畫一畫上面所說的過程,可以發現這個選擇過程其實就是一棵樹。
下面看看**,我們用visited來標記這個數是否訪問過,curnums是已經選取的數字集合,當它的size和nums的size一致時就結束遞迴。
class
solution
void
dfs(vectorint>>
& res,
const vector<
int>
&nums,vector<
int>
& visited,vector<
int>
& curnums)
for(
int i =
0; i < nums.
size()
;++i)}}
};
高階:leetcode47題。
這個問題題意也不難,求出乙個集合的所有子集,是高中的組合問題。
給定一組不含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)。這個問題比求解排列簡單一些,如果手算的話步驟如下:說明:解集不能包含重複的子集。
示例:輸入: nums = [1,2,3]
輸出:[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
]
(1)從nums中得到1,[1]是子集,加入結果
(2)從nums中得到2,[1,2]是子集,加入結果
(3)從nums中得到3,[1,2,3]是子集,加入結果
從第3步退回第2步,得到[1,3]子集加入結果、退回第1步從2開始搜尋即可。注意我們每次搜尋均從nums當前數字的下乙個數字開始搜尋,這樣確保結果不重複。
class
solution
void
dfs(vectorint>>
& res,
const vector<
int>
& nums,vector<
int>
& curnums,
int start)}}
;
高階:leetcode90題。
回溯法屬於難者不會會者不難的方法,打好遞迴基礎,做到心中可以模擬遞迴過程的話可以很快入門。
全排列和全組合
所謂全排列,就是列印出字串中所有字元的所有排列。例如輸入字串abc,則列印出 a b c 所能排列出來的所有字串abc acb bac bca cab和cba。一般最先想到的方法是暴力迴圈法,即對於每一位,遍歷集合中可能的元素,如果在這一位之前出現過了該元素,跳過該元素。例如對於abc,第一位可以是...
全排列和全組合實現
所謂全排列,就是列印出字串中所有字元的所有排列。例如輸入字串abc,則列印出 a b c 所能排列出來的所有字串abc acb bac bca cab和cba。一般最先想到的方法是暴力迴圈法,即對於每一位,遍歷集合中可能的元素,如果在這一位之前出現過了該元素,跳過該元素。例如對於abc,第一位可以是...
全排列和全組合實現
記得 老趙之前在微博上吐槽說,有的人真是毫無長進,六年前某同事不會寫程式輸出全排列,昨天發郵件還是問我該怎麼寫,這時間浪費到我都看不下去了。那時候就很好奇全排列到底是什麼東西,到底有多難?今天覆習的時候終於碰到這題了,結果果然自己太渣,看了好久都沒明白,實現又是磕磕碰碰的。所以,就把它整理成筆記加深...