回溯算法實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就 “回溯” 返回,嘗試別的路徑。回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術爲回溯法,而滿足回溯條件的某個狀態的點稱爲 “回溯點”。
回溯算法的基本思想是:從一條路往前走,能進則進,不能進則退回來,換一條路再試。
用回溯算法解決問題的一般步驟:
- 針對所給問題,定義問題的解空間,它至少包含問題的一個(最優)解。
- 確定易於搜索的解空間結構,使得能用回溯法方便地搜索整個解空間 。
- 以深度優先的方式搜索解空間,並且在搜索過程中用剪枝函數避免無效搜索。
下面我們通過幾道題來講解回溯算法的具體實現:
1.給定一個無重複元素的數組 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和爲 target 的組合。candidates 中的數字可以無限制重複被選取。
說明:
所有數字(包括 target)都是正整數。
解集不能包含重複的組合。示例 1:
輸入: candidates = [2,3,6,7], target = 7,
所求解集爲:
[
[7],
[2,2,3]
]示例 2:
輸入: candidates = [2,3,5], target = 8,
所求解集爲:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
思路:
- 假如輸入爲示例1,候選數組裏有 2 ,如果找到了 7 - 2 = 5 的所有組合,再在之前加上 2 ,就是 7 的所有組合;
- 同理考慮 3,如果找到了 7 - 3 = 4 的所有組合,再在之前加上 3 ,就是 7 的所有組合,依次這樣找下去;
- 上面的思路就可以畫成下面的樹形圖。
去掉文字的話如下:
如果按照上面的遞歸樹做的話還是達不到要求,因爲會產生重複。爲了避免重複,我們需要在每一次遞歸時不遍歷上一節點左邊的元素,如下圖,即我們不再遍歷綠色框住部分。
剪枝提速:
果一個數位搜索起點都不能搜索到結果,那麼比它還大的數肯定搜索不到結果。我們可以對輸入數組進行排序,以減少搜索的分支。上圖就是基於這個想法
回溯問題一般複雜度較高,能剪枝就儘量需要剪枝。把候選數組排個序,遇到一個較大的數,如果以這個數爲起點都搜索不到結果,後面的數就更搜索不到結果了。
c++實現如下:
class Solution {
private:
vector<int> candidates;//輸入數組
vector<vector<int>> res;//輸出數組
vector<int> path;//臨時數組
public:
void DFS(int start, int target) {//start爲遍歷起點,上面說到只遍歷start即其右邊的數
if (target == 0) {
res.push_back(path);
return;
}
for (int i = start;i < candidates.size() && target - candidates[i] >= 0; i++) {
path.push_back(candidates[i]);//將下一個數加入臨時數組
DFS(i, target - candidates[i]);
path.pop_back();//將上一次加入的數從臨時數組取出,以便回溯
}
}
//主函數
vector<vector<int>> combinationSum(vector<int> &candidates, int target) {
sort(candidates.begin(), candidates.end());
this->candidates = candidates;
DFS(0, target);
return res;
}
};
2.子集
給定一個可能包含重複元素的整數數組 nums,返回該數組所有可能的子集(冪集)。
說明:解集不能包含重複的子集。
示例:
輸入: [1,2,2]
輸出:
[ [2],
[1],
[1,2,2],
[2,2],
[1,2],
[ ] ]
以示例爲例,畫出樹狀圖。藍色爲有效節點,橙色爲重複節點。每次只遍歷上一節點右邊的元素。爲了找到重複的節點,我們可以先對數組進行排序,然後遍歷時如果同一輪添加數字相同時就跳過。
c++實現如下:
class Solution {
vector<vector<int>>v;
public:
void dfs(int start,vector<int>&nums,vector<int>&track)
{
v.emplace_back(track);//每搜索成功一次就添加一種組合
for(int i=start;i<nums.size();++i)
{
if(i!=start&&nums[i]==nums[i-1])continue;
track.emplace_back(nums[i]);
dfs(i+1,nums,track);
track.pop_back();//當前節點下搜索完成取出換另一節點
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<int>track;//臨時數組,用於存儲每一種組合
dfs(0,nums,track);
return v;
}
};
3.優美的排列
難度中等55假設有從 1 到 N 的 N 個整數,如果從這 N 個數字中成功構造出一個數組,使得數組的第 i 位 (1 <= i <= N) 滿足如下兩個條件中的一個,我們就稱這個數組爲一個優美的排列。條件:
第 i 位的數字能被 i 整除
i 能被第 i 位上的數字整除在給定一個整數 N,請問可以構造多少個優美的排列?
法一:我們產生所有可能的排列並對每一個排列都進行可除性檢查。此外,我們可以稍做優化。我們將每個元素添加到數組最後面的時候,我們馬上進行可除性檢查,一旦發現當前元素和位置不滿足要求我們就不能將這個元素放在當前位置,即可換一個元素繼續判斷。
class Solution {
int count=0;
public:
void dfs(vector<int>& v,int k)
{
if(k==v.size())count++;
for(int i=k;i<v.size();++i)
{
swap(v[i],v[k]);
if(v[k]%(k+1)==0||(k+1)%v[k]==0) dfs(v,k+1);
swap(v[k],v[i]);
}
}
int countArrangement(int N) {
vector<int>v;
for(int i=1;i<=N;++i)v.emplace_back(i);
dfs(v,0);
return count;
}
};
法二:使用visit數組記錄以使用數字,每次往添加數字從未訪問的節點裏面選擇,當數字長度未N時,即爲一種組合。
class Solution {
int count = 0;
int N;
public:
void dfs(vector<int>& v, vector<int>& visit,int tmp)//tmp記錄當前數組長度
{
if (v.size() == N)count++;//數組長度爲N時爲一種組合情況
for (int i = 0; i < N; ++i)//從N個數中選擇未被訪問過的數字放入數組第tmp個位置
{
if (visit[i] == 1)continue;
visit[i] = 1;//置1表示已訪問,在向字節點遍歷時將不會再使用此數
if ((i+1) % (tmp + 1) == 0 || (tmp + 1) % (i+1) == 0)
{
v.emplace_back(i + 1);
dfs(v, visit,tmp+1);
v.pop_back();
}
visit[i] = 0;//此次遍歷結束,重新置爲0
}
}
int countArrangement(int N) {
this->N = N;
vector<int>v;
vector<int>visit(N);
dfs(v, visit,0);
return count;
}
};
題目來源:leetcode
第一題圖片來源:https://leetcode-cn.com/u/liweiwei1419/