回溯算法 是一种穷举所有可能性的算法,也就是列举一棵决策树的所有路径
再说题目前,先说说选择层数和选择列表
如上所说,列举一棵树的所有路径,这里用[ a, b, c]的全排列来举个例子
蓝色的就是当前选择列表,红色的就是选择层数,一开始,选择列表为[a, b, c]所以我们有三种选择
而我们用递归实现回溯时,通常通过递归的调用进入下一层,而在回溯的函数里用for来列举当前选择列表
一般来说,使用回溯算法的题目可以分三种
三种在写法还是略有不同的,主要在结束条件上和for的初值等
顺便说一句,三种题目的剪枝条件都需要先对原序列进行排序才能正常进行
一:排列
排列的思路比较好理解
结束条件
一般都是路径到底部了,长度够了,就可以填入结果了
if(buf.size() == nums.size())
{
.......//填入结果
return ;
}
for的初值
都是从0开始,因为不能抛弃前面的点,比如我们从2开始[1,2,3]的排列,我们不能抛弃前面的1是吧?,不然就构不成一个完整的排列了,所以每一次选择列表都从0开始,但是在一个排列里我们也不希望出现aaa这种情况吧,所以在一条路径里,我们用一个数组来标记当前值是否被选中过,然后再for里判断,如果是选过的,就跳过
然后在走完一条路径后,撤销这些标记,不然下一条路径就不正常了
for(int i = 0;i<nums.size();++i)
{
if(flag[i]==true)continue;//如果被选过了就跳过
buf.push_back(nums[i]);//做出选择
flag[i] = true;//标记
................//递归调用
buf.pop_back();//撤销选择
flag[i] = fasle;//取消标记
}
剪枝
需要剪枝是因为在带排列的元素中可能出现重复元素,一旦出现了重复元素,在选择列表里就会有重复的待选项,而这些同一个选择列表里重复的项最后走完的路径是一样的,所以我们要排除掉重复的路径,选择列表是靠for枚举的,所以要在for里面,递归调用前判断是否是重复的
很简单
if(nums[i] == nums[i-1])continue;
但是别忘了i是从0开始的,所以i-1可能会越界,所以得加上一个i>0的条件
if(i > 0 && nums[i] == nums[i-1])continue;
但是这两个条件还是不够,还得加上一个flag[i-1] == fasle;
if(i > 0 && nums[i] == nums[i-1] && flag[i-1]==false)continue;
再结合之前的标记条件:
if(flag[i] == true ||(i > 0 && nums[i] == nums[i-1] && flag[i-1]==false))continue;
二:组合
结束条件
组合无顺序,但是数量确定了,比如在[1,2,3]中找出所有长度为2的组合,所以结束条件也是长度
if(buf.size() == n)
{
......//填入结果
return;
}
for的初值
因为组合无顺序要求,所以1,2,3和3,1,2没区别,所以我们不需要前面的数字,比如找[1,2,3]的长度为2的组合我们一开始肯定是找出[1,2]、[1,3]但是从2开始时,我们可能会得到[2,1]和[2,3],明显,[2,1]和[1,2]重复了,不是我们想要的 结果,那怎么办呢,很简单,在后续的选择列表里抛弃‘1’就行了呗,于是我们就从for的初值上下手,不要让1出现就好了,for就不能从0开始了,必须从上一层之后开始
排列里我们只跳过当前数字,因为已经选过了,而在组合里我们要跳过前面的所有数字,因为会重复
于是我们在回溯函数的参数里加上一个数来控制for的初值
void backtrack(.....其他参数,int idx)
{
//.....结束条件,填入结果
for(int i = idx;i<nums.size();++i)//控制初值
{
//.....做出选择
backtrack(...其他参数,i+1); //记住别传错了,是i+1,不是idx+1,不然就起不到去重的作用了
//.....撤销选择
}
}
剪枝:
如果待组合元素中有重复值,那我们还是需要剪枝,这里的剪枝和排列的剪枝写法很像,就是不需要判断最后的flag数组
if(i > idx && nums[i] == nums[i-1]) continue;
这里注意,不再是i > 0,而是i > idx,意义也一样,排列里的i>0是为了防止越界(数组的界和选择列表的界),因为选择列表是从0开始的,这里的i>idx也是防止越界,虽然我们抛弃了前面的数字,但他们实际上还是存在的,而在进行比较时,很可能会把当前列表里的数字与前面已经抛弃的数字进行比较,这样就可能会出错,导致去重不完整
三:子集
子集总体来说和组合差不多,for的初值和剪枝都一样
不一样的就是结束条件,因为子集,不需要考虑长度,所以直接填入就好了。
最后
以上的内容都是个模板而已,仅供参考,有些时候还得根据题意做出一些改变,但是大体思路没变,比如在二维数组得相关回溯题里,就可以用4个if来代替for进行选择列表的选择,对于边界值的限定也更方便,而在有些题目里,传for初值时,既不是0,也不是i+1,而是直接传i,因为当前值可以复用,等等,都是根据具体情况来的