回溯法方法以及例题(组合问题+排列问题)
方法模板框架
回溯函数模板返回值以及参数一般要根据题目的要求来进行编写,这里要注意的是参数一定是贯穿每层递归时要用到的值。
回溯函数终止条件既然是树形结构,遍历树形结构也就一定要有终止条件。所以回溯也有要终止条件。
回溯搜索的遍历过程这个可以结合树形结构来进行理解。
回溯法解决的问题都可以抽象为树形结构。
回溯算法模板框架如下:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小))
{
处理节点;
backtracking(路径,选择列表);
递归回溯,撤销处理结果;
}
}
例题:
组合是不强调元素顺序的,排列是强调元素顺序。例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了。记住组合无序,排列有序,就可以了。
组合问题:
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:输入: n = 4, k = 2
输出:[
[2,4], [3,4], [2,3], [1,2], [1,3], [1,4],
]
一开始还不太了解回溯算法的小伙伴可能会使用简单暴力的算法来试一下:从简单变成困难,所以就可以按示例来做一下,找找感觉,说不定就找到突破点了。
于是就有了如下代码:
#include<iostream>
using namespace std;
int main()
{
int n=4,k=2;
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)//不重复
cout<<i<<" "<<j<<endl;
}
}
运行结果如图:
咦,对比一下结果对了喔,我们不难发现:k的数值为多少,就有多少层for循环。再套到题目上,由于k会变动,而且数值大,太多for循环也不好写。所以呢单纯的暴力是解决不了。我们就可以想一下高级一点的暴力:递归回溯法。
代码如下:
#include<iostream>
#include<vector>
using namespace std;
int n=4,k=3;//做为全局变量 递归时就少传入一个参数
vector<int>some;//用于存储一组数
vector<vector<int>>all;//用于存储每组符合条件的数
void digui(int start)
{
if(k==some.size())//结束条件
{
all.push_back(some);//属于符合条件的存入
return;//退出递归
}
for(int i=start;i<=n;i++)//开始遍历
{
some.push_back(i);
digui(i+1);//不重复,所以+1
some.pop_back();//经典回溯
}
}
int main()
{
digui(1);
//这里还可以顺便复习一下vector<vector<int>>的输出方法的其中一种
for(vector<vector<int>>::iterator it = all.begin();it != all.end();it++)
{
for(vector<int>::iterator it0 = (*it).begin();it0 != (*it).end();it0++)
cout<<*it0<<" ";
cout<<endl;
}
}
运行结果如图:
排列问题:
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
输入:nums = [1,2,3]
输出:[
[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]
]
代码中有注释,加上有图片的理解下,这里就不多说了,排列问题其实和组合问题大同小异,只要记住了模板,这个问题还是很好解的。
小伙伴们还可以参考一下这张图片:
#include<iostream>
#include<vector>
using namespace std;
vector<int>nums= {1,2,3};
vector<bool>used(nums.size(),false);//用于判断是否遍历过
vector<int>some;//用于存储一组数
vector<vector<int>>all;//用于存储每组符合条件的数
void digui(vector<int>& nums,vector<bool>& used)
{
if(some.size()==nums.size())//结束条件
{
all.push_back(some);//属于符合条件的存入
return;//退出递归
}
for(int i=0; i<nums.size(); i++)//开始遍历
{
if(used[i]==true)continue;//遍历过的直接跳过到下一个i
used[i]=true;//标记这个是遍历过的
some.push_back(nums[i]);//存入一组数中
digui(nums,used);//递归未遍历过的
some.pop_back();//回溯
used[i]=false;//回溯
}
}
int main()
{
digui(nums,used);
//这里还可以顺便复习一下vector<vector<int>>的输出方法的其中一种
for(vector<vector<int>>::iterator it=all.begin();it!=all.end();it++)
{
for(vector<int>::iterator it0=(*it).begin();it0!=(*it).end();it0++)
cout<<*it0<<" ";
cout<<endl;
}
}
运行结果如图:
总结:
回溯的方法多用于暴力递归和二叉树中的问题中,当然其它特殊的情况下也是有用到过的,学习理解了回溯递归的方法,相信很多的问题也就能迎刃而解了。希望看到这篇笔记的小伙伴们能理解其中的奥秘,good good study!day day up!