本文從以下幾個方面對回溯算法進行總結:
1、什麼是回溯算法
2、回溯算法與窮舉法的區別與聯繫
3、回溯算法的解題步驟(準備工作、解題步驟、遞歸方法的參數選擇)
4、例題:LeetCode-39 組合總和、LeetCode-40 組合總和Ⅱ
5、例題整理
一、什麼是回溯算法
回溯算法(百度百科)實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術爲回溯法,而滿足回溯條件的某個狀態的點稱爲“回溯點”。許多複雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。
二、回溯算法與窮舉法的區別與聯繫
回溯法與窮舉法有某些聯繫,它們都是基於試探的,但是回溯算法是優於窮舉法的。窮舉法要將一個解的各個部分全部生成後,才檢查是否滿足條件,若不滿足,則直接放棄該完整解,然後再嘗試另一個可能的完整解,它並沒有沿着一個可能的完整解的各個部分逐步回退生成解的過程。而對於回溯法,一個解的各個部分是逐步生成的,當發現當前生成的某部分不滿足約束條件時,就放棄該步所做的工作,退到上一步進行新的嘗試,而不是放棄整個解重來。(所謂剪枝,也就是避免了一些不必要的搜索)
三、回溯算法的解題步驟
準備工作:
1)由於採用回溯法求解時存在退回到祖先結點的過程,所以需要保存搜索過的結點。
通常有兩種方法:1、用子定義棧來保存;2、採用遞歸方法。
2)用回溯法通常採用兩種策略避免無效搜索。這兩類函數統稱爲剪枝函數。
1、用約束函數在擴展結點處剪除不滿足約束條件的路徑;
2、用限界函數剪去得不到問題的解或最優解的路徑。
3)要保證遞歸函數返回 或 彈出棧頂元素 後,狀態可以恢復到操作前,以此達到真正的回溯。
回溯法求解的一般步驟:
1、針對給定的問題確定問題的解空間樹
2、確定結點的擴展搜索規則。
3、以深度優先方式搜索解空間樹,並在搜索過程中可以採用剪枝函數來避免無效搜索。其中,深度優先方式可以採用遞歸回溯或者非遞歸(迭代)回溯。
遞歸方法的參數選擇:
1、一個臨時變量(可以就直接傳遞一個字面量或者常量進去)傳遞不完整的解,因爲每一步選擇後,暫時還沒構成完整的解,這個時候這個選擇的不完整解,也要想辦法傳遞給遞歸函數。也就是,把每次遞歸的不同情況傳遞給遞歸調用的函數。
2、一個全局變量,用來存儲完整的每個解,一般是個集合容器(也不一定要有這樣一個變量,因爲每次符合結束條件,不完整解就是完整解了,直接打印即可)。
3、最重要的一點,一定要在參數設計中,可以得到結束條件。一個選擇是可以傳遞一個量n,也許是數組的長度,也許是數量,等等。
四、例題
LeetCode-39 組合總和 題目鏈接:https://leetcode-cn.com/problems/combination-sum/
給定一個無重複元素的數組
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] ]
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(candidates);
combinationSum(candidates, target, 0, new ArrayList<>(), result);
return result;
}
// 回溯算法
// 1、題目爲無重複數組,故不需去重
// 2、數字可被無限重複選取,故從start本身開始遞歸
public static void combinationSum(int[] candidates, int target, int start, List<Integer> temp, List<List<Integer>> result) {
if (target == 0) {
result.add(new ArrayList<>(temp));
return;
}
if (target < 0) {
return;
}
for (int i = start; i < candidates.length && candidates[i] <= target; i++) {
temp.add(candidates[i]);
combinationSum(candidates, target - candidates[i], i, temp, result);
temp.remove(temp.size()-1);
}
}
LeetCode-40 組合總和Ⅱ 題目鏈接:https://leetcode-cn.com/problems/combination-sum-ii/
給定一個數組
candidates
和一個目標數target
,找出candidates
中所有可以使數字和爲target
的組合。
candidates
中的每個數字在每個組合中只能使用一次。說明:
- 所有數字(包括目標數)都是正整數。
- 解集不能包含重複的組合。
示例 1:
輸入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集爲: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
示例 2:
輸入: candidates = [2,5,2,1,2], target = 5, 所求解集爲: [ [1,2,2], [5] ]
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(candidates);
combinationSum2(candidates, target, 0, new ArrayList<>(), result);
return result;
}
// 回溯算法
// 1、題目爲重複數組,故需要去重
// 2、數字只能選取一次,故從start+1開始遞歸
public static void combinationSum2(int[] candidates, int target, int start, List<Integer> temp, List<List<Integer>> result) {
if (target < 0) {
return;
}
if (target == 0) {
result.add(new ArrayList<>(temp));
return;
}
for (int i = start; i < candidates.length && candidates[i] <= target; i++) {
if (i > start && candidates[i] == candidates[i-1]) // 去重
continue;
temp.add(candidates[i]);
combinationSum2(candidates, target - candidates[i], i+1, temp, result);
temp.remove(temp.size()-1);
}
}
五、例題整理
1、LeetCode-39 組合總和 題目鏈接:https://leetcode-cn.com/problems/combination-sum/
2、LeetCode-40 組合總和Ⅱ 題目鏈接:https://leetcode-cn.com/problems/combination-sum-ii/
3、LeetCode-46 全排列 題目鏈接:https://leetcode-cn.com/problems/permutations/
4、LeetCode-47 全排列Ⅱ 題目鏈接:https://leetcode-cn.com/problems/permutations-ii/
5、LeetCode-78 子集 題目鏈接:https://leetcode-cn.com/problems/subsets/
6、LeetCode-90 子集Ⅱ 題目鏈接:https://leetcode-cn.com/problems/subsets-ii/