前言
回溯,顾名思义就是回到之前的状态,常见的有时空回溯(祖安小伙艾克)以及今天要了解的回溯算法
回溯算法是一种通过探索所有可能的候选解来找出所有解的通用算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会放弃该解,回退到上一步,并尝试其他的可能性。
回溯算法可以看成是一种有组织的穷举方法,算是暴力算法的优化吧
有以下特点:
1.系统性:会按照某种顺序系统性的探索空间
2.跳跃性:当发现当前的路径并不能得到解时,立即立即回溯,返回上一路径
概念听的有点困了,还是来看看题吧家人们
迎面向我们走来的时无重复项数字全排列!
无重复项数字全排列
既然有无重复项的那肯定就存在有重复项的,这个我们先按下不表
题意是给一组数字,然后返回数字的所有排列
思路讲解
这里刚好适用回溯算法
其实这类使用回溯算法的还有一个模板,相似的题目可以直接套用
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
这里使用的是b站大佬代码随想录讲的模板,他讲算法很清晰透彻,如果真有人看我这篇文章的话,也可以去b站看看人家讲算法
那么回到题目,这道题无非就是递归找出所有可能嘛
由于题目要返回的是多组数字,所以首先需要定义一个二维数组来存放最终要返回的结果;
vector<vector<int>> res;
当然,这个数组得是个全局数组,因为每次找到结果后就要存放进去
接着还需要一个一维数组来存放单组数据
vector<int> path;
最后,还需要一个布尔类型的数组,表示当前数字再这个路径下有没有被使用,而且要初始化为false
vector<bool> used (num.size(),false);
前置工作做完了,就到代码的核心部分了, 首先明确的是,回溯使用了递归来进行实现,那么递归就有终止条件,这里的终止条件就是——path数组存满时,这时就表示找到了一钟排列方式;
所以终止条件就是当path数组的长度等于了题目给定数组的长度时,就向最终数组里存放一组数据
然后到下面的循环环节,循环次数肯定得是总数组的大小
然后还得判断,当前数字是否被使用过,因为在第二层循环时,数组中的第一个数字已经被使用了,所以就要加上判断,防止第一个数字被重复使用;如果当前的数字被使用过了,则跳过这次循环,进入下一层;
当数字未被使用过时,就要向path数组里存放,然后used数组也要标记该数字被使用了,=
然后,再进行递归,找path数组中的下一个元素
接着在递归结束后还要进行回溯,path数组pop,然后used数组讲当前位置赋为false
差不多是这样吧
代码:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>num,vector<bool>used)
{
//递归终止条件
if(path.size()==num.size())
{
res.push_back(path);
return;
}
//一步步遍历
for(int i=0;i<num.size();i++)
{
//如果当前位置已经被使用过,直接跳过当前循环
if(used[i]==true) continue;
path.push_back(num[i]);
used[i]=true;
backtracking(num, used);
//进行回溯
used[i]=false;
path.pop_back();
}
}
vector<vector<int> > permute(vector<int>& num)
{
vector<bool> used (num.size(),false);
backtracking(num, used);
return res;
}
然后下面这道题时是上面的延申
有重复项的全排列
思路讲解
其实和上一道题总体思路一样,甚至代码80%都一样,只是加了一点处理
首先,有重复项就说明如果不去重到时候就会有重复的排列,所以一定要去重
那该怎么去重呢
我们假设排序后的数组为 [a, a, b]
,去重逻辑的工作方式:
-
第一个a被使用,标记为已使用
-
遇到第二个a时:
-
如果前一个a未被使用(即回溯后被撤销的状态),说明我们正在尝试以不同的顺序使用相同元素
-
这时跳过当前a,避免生成重复排列
-
所以只需要在循环判断时加上去重逻辑就行,当然为了方便去重,可以用sort函数先进行排序
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>num,vector<bool>used)
{
if(path.size()==num.size())
{
res.push_back(path);
return;
}
for(int i=0;i<num.size();i++)
{
//如果上一个元素和当前元素一样,且未被标记使用,则需要进行去重
if(i>0&&used[i-1]==false&&num[i]==num[i-1]) continue;
if(used[i]==true) continue;
path.push_back(num[i]);
used[i]=true;
backtracking(num, used);
path.pop_back();
used[i]=false;
}
}
vector<vector<int> > permuteUnique(vector<int>& num)
{
sort(num.begin(), num.end());
vector<bool> used(num.size(),false);
backtracking(num, used);
return res;
}
下面这道题基本类似,刚好都讲了
字符串的排列
题目大意和数字一样,只是换成了字符串
思路讲解
思路无二,但是也需要做些修改,首先path不能是数组了,因为返回值是string类型的数组,所以res才是数组,path只能当个string
除了会有重复元素所以需要去重和排序外,似乎没有什么区别
vector<string> res;
string path;
void backtracking(string& str, vector<bool>used) {
if (path.size() == str.size()) {
res.push_back(path);
return;
}
for (int i = 0; i < str.size(); i++) {
if (used[i] == true) continue;
if(str[i]==str[i-1]&&used[i-1]) continue;
used[i] = true;
path.push_back(str[i]);
backtracking(str, used);
used[i] = false;
path.pop_back();
}
}
vector<string> Permutation(string str) {
sort(str.begin(),str.end());
vector<bool> used(str.size(), false);
backtracking(str, used);
return res;
}