1、什麼是回溯法
wiki上面是這麼說的
在包含問題的所有解的解空間樹中,按照深度優先搜索的策略,從根結點出發深度探索解空間樹。當探索到某一結點時,要先判斷該結點是否包含問題的解,如果包含,就從該結點出發繼續探索下去,如果該結點不包含問題的解,則逐層向其祖先結點回溯。(其實回溯法就是對隱式圖的深度優先搜索算法)。 若用回溯法求問題的所有解時,要回溯到根,且根結點的所有可行的子樹都要已被搜索遍才結束。 而若使用回溯法求任一個解時,只要搜索到問題的一個解就可以結束。
其實通俗點講我覺得回溯法就是使用在對一組數據求其在某種條件下所有的可能性組合。
2、通用框架
常見的題目通常是給一個數組數據或者字符串,然後求其所有組成。所以有如下的通用解題模板
Class Solation{
public List<List<Integer>> backtrack(int[] a)
{
List<List<Integer>> list = new ArrayList<>();
//Arrays.sort(a);//當a中存在重複值。而重複值不能使用的時候。就要進行排序。對使用過的重複值不再使用
backTrackTemp(list, new ArrayList<>(), a, .....)//其中的"...."表示其他限定條件(根據條件限定而存在與否)
return lis
}
//回溯過程
private static void backTrackTemp(List<List<Object>> list, Arraylist<Integer> tempList, int[] a,....)
{
//終止條件,也就是一次結果或者不符合條件
if(false)//false代表條件不符合
return false;
if(true)//當符合需要的結果
list.add(new ArrayList(tempList))//注意這裏要重新創建,因爲tempList是一個對象,改變的話,會改變結果值。所以重新創建
//對每個值進行回溯
for(int i = start; i < a.length; i++)
{
if(true)//存在某個限定條件的,比如出現重複值,跳過(根據條件限定而存在與否)
continue;
mask(used(i));//將i標記爲已使用(根據條件限定而存在與否)
backTrackTemp(list, tempList, a, i+1)//此處的i+1也可以根據實際情況判斷題目中的數字是否可以重複使用
unmask(used(i));//回溯完要記得取消掉
tempList.remove(tempList.size() - 1);//回溯回父節點.尋找下一個節點
}
}
}
這裏對backTrackTemp(list, new ArrayList<>(), a, .....)
中的參數說個說明
1、list:是用來保存最終結果集
2、new ArrayList<>()是用來保存中間產生的結果集
3、a,給定條件
4、…其他一些限定條件
前面三個條件是必須存在的。後面的4根據題目要求而進行的更改
接下來我們通過一些例子來看看是怎麼使用這個demo
的
3、模板測試
1、subSet
1、數組中不存在重複數值的
題目如下:
Given a set of distinct integers, nums, return all possible subsets (the power set).
Note: The solution set must not contain duplicate subsets.
Example:
Input: nums = [1,2,3]
Output:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
backTracking(list,new ArrayList<>(),nums,0);//list,臨時list,額外條件,因爲subSet是不能存在重複的,所以要有start值。因爲在set中只要元素相同,即使順序不用也是一致的。跟排列不一樣
return list;
}
//沒有start就找不到終止條件了
private static void backTracking(List<List<Integer>> list, ArrayList<Integer> tempList, int[] nums,int start){
list.add(new ArrayList<>(tempList));//每一組都添加,不用條件,這一步已經把空的也加進去
for(int i = start; i < nums.length; i++)
{
tempList.add(nums[i]);
backTracking(list,tempList,nums,i+1);
tempList.remove(tempList.size() - 1);
}
}
}
2、數組中存在重複數值的,但是結果不能存在重複值
Given a collection of integers that might contain duplicates, nums, return all possible subsets (the power set).
Note: The solution set must not contain duplicate subsets.
Example:
Input: [1,2,2]
Output:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backTracking(list, new ArrayList<>(), nums, 0);
return list;
}
//因爲這個回溯法是判斷以每一個數爲準的所有組成
private static void backTracking(List<List<Integer>> list, List<Integer> tempList, int[] nums, int startIndex)
{
//方法一:判斷是否已經存在
// if(!list.contains(tempList))
list.add(new ArrayList<>(tempList));
for(int i = startIndex; i < nums.length; i++)
{
if(i > startIndex && nums[i] == nums[i-1]) continue;//方法二.跳過重複的數字
tempList.add(nums[i]);
backTracking(list, tempList, nums, i + 1);
tempList.remove(tempList.size()-1);
}
}
}
2、Permutations
1、不存在重複值的排列
Given a collection of distinct integers, return all possible permutations.
Example:
Input: [1,2,3]
Output:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
class Solution {
public static List<List<Integer>> permute(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
backtrack(list, new ArrayList<>(), nums,new boolean[nums.length]);//回溯法的三要素,條件值nums,存儲結果數組list,臨時結果(判斷用的)。還有其他的額外控制條件
return list;
}
private static void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, boolean[] used){
if(tempList.size() == nums.length){//終止條件
list.add(new ArrayList<>(tempList));//這邊要new的原因是因爲下面的對象引用還存在。更改會改變這個list裏面的值
} else{
for(int i = 0; i < nums.length; i++){//回溯過程
if(used[i] == true)
continue;
else {
tempList.add(nums[i]);
used[i] = true;//訪問過了就不要再訪問
backtrack(list, tempList, nums, used);
used[i] = false;//一次回溯後就要將訪問的重新置爲未訪問的
tempList.remove(tempList.size() - 1);
}
}
}
}
}
2、存在重複數字
Given a collection of numbers that might contain duplicates, return all possible unique permutations.
Example:
Input: [1,1,2]
Output:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backTracking(list, new ArrayList<>(), nums, new boolean[nums.length]);
return list;
}
private void backTracking(List<List<Integer>> list, ArrayList<Integer> tempList, int[] nums, boolean[] used){
//終止條件
//if(tempList.size() == nums.length && !list.contains(tempList))//用contains判斷是否已經存在
if(tempList.size() == nums.length)
list.add(new ArrayList(tempList));
//回溯條件
for(int i = 0; i < nums.length; i++)
{
if((i > 0 && nums[i] == nums[i - 1] && used[i-1]==false)||(used[i] == true))//訪問過了就不再訪問且判斷重複的去掉
continue;
tempList.add(nums[i]);//添加到tempList中
used[i] = true;//置爲訪問過的
backTracking(list, tempList, nums, used);//以當前點回溯
used[i] = false;
tempList.remove(tempList.size() - 1);//返回上一層
}
}
}
3、 Combination Sum
1、不存在重複值的
Given a set of candidate numbers (candidates) (without duplicates) and a target number (target), find all unique combinations in candidates where the candidate numbers sums to target.
The same repeated number may be chosen from candidates unlimited number of times.
Note:
All numbers (including target) will be positive integers.
The solution set must not contain duplicate combinations.
Example 1:
Input: candidates = [2,3,6,7], target = 7,
A solution set is:
[
[7],
[2,2,3]
]
Example 2:
Input: candidates = [2,3,5], target = 8,
A solution set is:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> list = new ArrayList<>();
backTracking(list, new ArrayList<>(), candidates, target, 0);
return list;
}
private static void backTracking(List<List<Integer>> list, ArrayList<Integer> tempList, int[] candidates, int target, int startIndex)
{
if(target < 0)return;//當不符合的時候,返回
if(target == 0)list.add(new ArrayList<>(tempList));//符合的時候添加到集合
for(int i = startIndex; i < candidates.length; i++)
{
tempList.add(candidates[i]);
//回溯,因爲一個節點可以重複使用,所以還是以i爲當前節點
backTracking(list, tempList, candidates, target - candidates[i], i);
tempList.remove(tempList.size() - 1);
}
}
}
2、存在重複值的
Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sums to target.
Each number in candidates may only be used once in the combination.
Note:
All numbers (including target) will be positive integers.
The solution set must not contain duplicate combinations.
Example 1:
Input: candidates = [10,1,2,7,6,1,5], target = 8,
A solution set is:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
Example 2:
Input: candidates = [2,5,2,1,2], target = 5,
A solution set is:
[
[1,2,2],
[5]
]
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(candidates);
backTracking(list, new ArrayList<>(), candidates, target, 0);
return list;
}
private static void backTracking(List<List<Integer>> list, ArrayList<Integer> tempList, int[] candidates, int target, int startIndex )
{
if(target < 0)return;
if(target == 0)list.add(new ArrayList<>(tempList));//終止條件
for(int i = startIndex; i < candidates.length; i++)
{
if(i > startIndex && candidates[i] == candidates[i-1])//去掉重複的
continue;
tempList.add(candidates[i]);
backTracking(list, tempList, candidates, target - candidates[i],i+1);//每個數字只能用一次
tempList.remove(tempList.size() - 1);
}
}
}
4、Palindrome Partitioning
Given a string s, partition s such that every substring of the partition is a palindrome.
Return all possible palindrome partitioning of s.
Example:
Input: "aab"
Output:
[
["aa","b"],
["a","a","b"]
]
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> list = new ArrayList<>();
backTracking(list, new ArrayList<>(), s, 0);
return list;
}
private static void backTracking(List<List<String>> list, List<String> tempList, String s, int startIndex){
//終止條件,也就是加入list的條件,startIndex來到最後一個字符。遍歷完了
if(startIndex == s.length())
list.add(new ArrayList<>(tempList));
for(int i = startIndex; i < s.length(); i++)
{
if(isPartition(s, startIndex, i))//如果是迴文串,
{
tempList.add(s.substring(startIndex,i+1));//加入到templist.substring(start, end),end是取不到的
backTracking(list, tempList, s, i+1);//當前部分是迴文串就不會再使用
tempList.remove(tempList.size() - 1);
}
}
}
//判斷是否是迴文串
private static boolean isPartition(String s, int left, int right)
{
while(left <= right)
{
if(s.charAt(left++) != s.charAt(right--))
return false;
}
return true;
}
}
4、總結
我們通過subset(所有可能子集合),Permutations(所有可能排序), Combination Sum(可以組合成一個數的所有情況)、Palindrome Partitioning(一個字符串拆成都是迴文串的所有個數)。
1、從中可以發現。如果跟順序沒有關係的,則應該加startIndex,而如果有關係則都應該從0開始。因爲不同位置從0開始都是不同的。
2、如果在一次中訪問過的可能再訪問到且不能再訪問,則應該設置一個used[]數組來保存每次訪問過的元素。當一次回溯完時則應該放開。
3、如果存在重複的數字。則應該先排序,排序可以更好的解決去掉重複值的情況。