回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就 “回溯” 返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为 “回溯点”。
回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
用回溯算法解决问题的一般步骤:
- 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
- 确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
- 以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
下面我们通过几道题来讲解回溯算法的具体实现:
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