1、概念
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
2、基本思想
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
3、算法框架
主要看看递归框架:
//一个保存最终结果的数组或容器,一般是vector,假设命名为 rvec
//一个保存临时单个结的数组或容器,一般也是vector,假设命名为 temp
void backtrack(.....) // 参数一般是包含源数组,最红结果数组,临时数组,以及你的目标,数组下标等
{
if(条件) //看实际情况,有可能有条件也可能没有
rvec保存这个结果;
else
{
for(j = 下界; j <= 上界; j=j+1) // 枚举i所有可能的路径
{
if(条件) //看实际情况,有可能有条件,也可能没有
{
temp.push_back(值); //temp保存这个值
backtrack(.....); //主意看你是否可重复用数组元素,下标分别为原下标,或下边加一。
temp.pop_back(); //回溯前的清理工作(如a[i]置空值等);
}
}
}
}
上述框架基本可以解决大部分的问题了,下面看看用这个框架解决实际问题。
4、实际案例
1、Subsets
地址:https://leetcode.com/problems/subsets/
问题描述:给一个不存在重复数字的数组,找到所有可能的子数组。比如:
If nums = [1,2,3], a solution is:[ [3],[1],[2],[1,2,3],[1,3],[2,3],[1,2],[] ]
代码:
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums)
{
vector<vector<int>> resvec;
vector<int> tmpvec;
sort(nums.begin(),nums.end());
backtrack(resvec,tmpvec,nums,0);
return resvec;
}
void backtrack(vector<vector<int>> &resvec,vector<int> tmpvec,vector<int> nums,int start)
{
resvec.push_back(tmpvec);
for(int i=start;i<nums.size();i++)
{
tmpvec.push_back(nums[i]);
backtrack(resvec,tmpvec,nums,i+1);
tmpvec.pop_back();
}
}
};
2、Subsets II
地址:https://leetcode.com/problems/subsets-ii/
问题描述:给一个包含重复数字的数组,找到所有可能的子数组。比如:
If nums = [1,2,2], a solution is:[ [2],[1],[1,2,2],[2,2],[1,2],[] ]
代码:
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums)
{
vector<vector<int>> rvec;
vector<int> temp;
sort(nums.begin(),nums.end());
backtrace(rvec,temp,nums,0);
return rvec;
}
void backtrace(vector<vector<int>> &rvec,vector<int> &temp,const vector<int> &nums,int start)
{
rvec.push_back(temp);
for (int i=start;i<nums.size();i++)
{
if(i==start||nums[i]!=nums[i-1])
{
temp.push_back(nums[i]);
backtrace(rvec,temp,nums,i+1);
temp.pop_back();
}
}
}
};
3、Permutations
地址:https://leetcode.com/problems/permutations/
问题描述:给一个不存在重复数字的数组,找到所有的排列。比如:
For example,[1,2,3] have the following permutations: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
代码:
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums)
{
vector<vector<int>> revec;
vector<int> temp;
backtrace(nums,temp,revec);
return revec;
}
void backtrace(vector<int>& nums,vector<int> temp,vector<vector<int>> &revec)
{
if(temp.size()==nums.size())
revec.push_back(temp);
else
{
for(int i=0;i<nums.size();i++)
{
if(find(temp.begin(),temp.end(),nums[i])==temp.end())
{
temp.push_back(nums[i]);
backtrace(nums,temp,revec);
temp.pop_back();
}
}
}
}
};
4、Permutations II (contains duplicates)
地址:https://leetcode.com/problems/permutations-ii/
问题描述:给一个包含重复数字的数组,找到所有的排列。比如:
For example, [1,1,2] have the following unique permutations: [ [1,1,2], [1,2,1], [2,1,1] ]
代码:
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums)
{
vector<vector<int>> revec;
vector<int> temp;
vector<bool> used(nums.size());
sort(nums.begin(), nums.end());
backtrace(nums, temp, revec,used);
return revec;
}
void backtrace(vector<int>& nums, vector<int> temp, vector<vector<int>> &revec, vector<bool> used)
{
if (temp.size() == nums.size())
revec.push_back(temp);
for (int i = 0; i<nums.size(); i++)
{
if (used[i] || i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
temp.push_back(nums[i]);
used[i] = true;
backtrace(nums, temp, revec,used);
used[i] = false;
temp.pop_back();
}
}
};
5、Combination Sum
地址:https://leetcode.com/problems/combination-sum/
问题描述:给一个不包含重复数字的数组和一个目标数字,找到所有和等于目标数字的排列。比如:
For example, given candidate set [2, 3, 6, 7] and target 7, A solution set is: [ [7], [2, 2, 3] ]
代码:
class Solution {
public:
void backtracing(vector<int> &candidates, vector<int> temp,vector<vector<int>> &result,int target, int start )
{
if (target<0) return;
else if (target == 0) result.push_back(temp);
else
{
for (int i = start; i < candidates.size(); i++)
{
temp.push_back(candidates[i]);
backtracing(candidates,temp,result, target-candidates[i],i); // not i + 1 because we can reuse same elements
temp.pop_back();
}
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
vector<vector<int>> result;
vector<int> temp;
sort(candidates.begin(), candidates.end());
backtracing(candidates, temp,result,target, 0);
return result;
}
};
6、Combination Sum II
地址:https://leetcode.com/problems/combination-sum-ii/
问题描述:给一个包含重复数字的数组和一个目标数字,找到所有和等于目标数字的排列,每个数字不能用多次,但是出现在数组中的可以用。比如:
For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8, A solution set is: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
代码:
class Solution {
public:
void backtracing(vector<int> &candidates,vector<int> temp, vector<vector<int>> &result, int target, int start)
{
if (target <0) return;
else if (target == 0) result.push_back(temp);
else
{
for (int i = start; i < candidates.size(); i++)
{
if(i>start&&candidates[i]==candidates[i-1]) continue; // skip duplicates otherwise will like this [1,7] [1,7]
temp.push_back(candidates[i]);
backtracing(candidates, temp, result,target-candidates[i], i+1 );
temp.pop_back();
}
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
{
vector<vector<int>> result;
vector<int> temp;
int sum=0,levle = 0;
sort(candidates.begin(), candidates.end());
backtracing(candidates,temp, result, target, 0);
return result;
}
};
7、Combination Sum III
地址:https://leetcode.com/problems/combination-sum-iii/
问题描述:给一个包含重复数字的数组和一个目标数字,以及一个排列长度,找到所有和等于目标数字的规定长度的排列,每个数字不能用多次,但是出现在数组中的可以用。比如:
Input: k = 3, n = 7 ,Output:[[1,2,4]]
Input: k = 3, n = 9 ,Output:[[1,2,6], [1,3,5], [2,3,4]]
代码:
class Solution {
public:
void backtracing(const vector<int> &candidates,vector<int> temp, vector<vector<int>> &result, int k,int target, int start)
{
if (target<0) return;
else if (target==0)
{
if(temp.size()==k)
result.push_back(temp);
return;
}
else
{
for (int i = start; i < candidates.size(); i++)
{
if(i>start&&candidates[i] == candidates[i + 1]) continue;
{
temp.push_back(candidates[i]);
backtracing(candidates,temp, result, k,target-candidates[i], i+1);
temp.pop_back();
}
}
}
}
vector<vector<int>> combinationSum3(int k, int n)
{
vector<vector<int>> result;
vector<int> temp;
vector<int> candidates = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
backtracing(candidates,temp, result,k,n, 0);
return result;
}
};
8、Palindrome Partitioning
地址:https://leetcode.com/problems/palindrome-partitioning/
问题描述:给一个字符串,将s分成每个子串都是回文的所有排列组合。比如:
For example, given s = “aab”,Return :[ [“aa”,”b”], [“a”,”a”,”b”] ]
代码:
class Solution {
public:
vector<vector<string>> partition(string s)
{
vector<vector<string>> rvec;
if(s.empty()) return rvec;
vector<string> temp;
backtrace(s,temp,rvec,0);
return rvec;
}
void backtrace(const string &s,vector<string> temp,vector<vector<string>> &rvec,int index)
{
if(index==s.length())
{
rvec.push_back(temp);
return;
}
for(int i=index;i<s.length();i++)
{
if(isPalindrome(s,index,i))
{
temp.push_back(s.substr(index,i-index+1));
backtrace(s,temp,rvec,i+1);
temp.pop_back();
}
}
}
bool isPalindrome(const string &s,int low,int high)
{
while(low<=high)
{
if(s[low++]!=s[high--])
return false;
}
return true;
}
};
上面都是C++版的,leetcode上有java版的,见下面参考资料。
5、参考资料
本文章主要来源于LeetCode,这里对这个平台表示感谢,强烈推荐大家可以去刷刷题,不仅仅为找工作,更重要的锻炼自己的解决问题的能力,同时也可以跟各路算法大神互动交流。地址:A general approach to backtracking questions in Java (Subsets, Permutations, Combination Sum, Palindrome Partioning)
开始的对于回溯算法概念以及框架的来源于网上一位名叫“红脸书生”的大神,我稍稍对算法框架做了修改。
地址:五大常用算法之四:回溯法