回溯算法简介
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
应用场景
一般回溯的问题有四种:
- Find a path to success 有没有解
- Find all paths to success
- 求所有解
求所有解的个数
求所有解的具体信息
Find the best path to success 求最优解 - 求限定条件的所有解
例如求一个数组满足某种条件的子数组
结合代码和示例超详细分析
我其实还没有学到数据结构中的的树结构,但是下面我讲的你没学过也看得懂,只要会循环和递归就能看懂,至于什么广度深度这些偏术语的东西我就不讲了。
这里最基础的应用求所有解中的全排列问题来做分析
(其他的问题基本上就是在这种问题上加上限定条件再做筛选)
算法实现:(c++)
void trackBack( vector<int>nums, vector<int>& flags, vector<int>& track)
//这三个参数分别是:你传入的要全排列的数组 要全排列的元素是否被添加到track数组中的标记 一次排列情况 这里我们用一个整型数组来做标记 如果flags[i]=1则表示 nums[i]被添加到track中了 0则表示没有添加到tarck中
{
//判断一次排列是否已经完成
if (track.size() == nums.size())
{
res.push_back(track);//一次深度遍历等到一条路径添加到结果集中
return ;
}
for (int i = 0; i <nums.size(); i++)
{
if (flags[i])
continue;
track.push_back(nums[i]);//添加元素到路径中
flags[i] = 1;//标记
trackBack(nums, flags, track);//递归调用回溯函数
track.pop_back();//退格元素
flags[i] = 0;//还原标记
}
}
下面用1 2 3的全排列作为示例分析 F一1表示第一层循环的第一次有效循环(有效指的是往track数组中添加了元素的循环)
其中有效循环指添加进了一个元素 循环的层数是指循环所在的函数是递归的层数加1
比如 void func()//第0层递归
{
for(int i=0;i<3;i++)//第0层递归函数中的循环 所以就是第一层循环 有三次
{
func();//这个就是第一层递归 由于循环有三次所以这里有三个第一层递归 第一层递归中的循环就是第二层循环 第二层递归中的循环就是三层循环
那么以此类推被第一层递归调用的就是第二层递归
}
}
好定义好这些了方便描述的关键词之后 进入下面 1 2 3的全排列分析
- 这里我们传入nums{1,2,3};而且传入 flags{0,0,0}
然后调用上面的trackBack()函数 这次调用是第0次递归调用 此时track中还没有添加进元素
当然那个if()判断就跳过了 ,执行下面的第一层循环的第一次循环F一1
进入循环F一1 flags[0]为0所以继续向下执行添加nus[0] flags[0]=1;所以此时track中有一个元素’1’
继续向下执行就到了第一次递归所以进入第一层递归函数 - 跳过if()向下执行到了第二层循环 这是第一层循环的第一次循环的第二层循环即F一1二
然后进行第一次有效循环 F一1二1 (i=0是无效循环因为此时track中有了元素’1’所以第一次循环 通过if判断continue掉了,下面我就不加有效了默认为有效循环)
添加了nums[1] flags[1]=1 所以此时track中有了两个元素1 2 - 然后继续向下执行到了递归 这是第三层递归 所以进入递归函数(第二层递归) if条件不满足 继续向下执行 到了循环 这是第三层循环 而且是第一层循环的第一次循环的第二层循环的第一次循环的第三层循环 执行第一次循环 添加 nums[2] flags[2]=1 此时track中有了三个元素 1 2 3
- 继续向下又到了递归第三层递归 进入递归函数 满足if()判断条件track.size()=nums.size() 说明一次排列已经完成所以把此时的track添加到结果集res中 然后return 退出这个函数
- 而这个函数是在F一1二1三1中调用的 所以也就是F一1二1三1中的trackBack()执行完了 所以继续向下执行 此时的i=2 track.pop_back(); 把最后一个元素3踢出 flags[2] = 0;还原标记 然后F一1二1三1就执行完了 又因为这次循环已经是这层循环的最后一次循环 所以也就是所以这层循环就结束了 所以他所在的函数 第二层递归也就结束了 而这个第二层递归函数又是F一1二1中调用的 所以结束之后返回到F一1二1中递归调用的下一句 此时的i=1 track.pop_back();把最后一个元素2踢出 flags[1] = 0;还原标记 所以F一1二1执行完 也就是第一层循环的第一次循环的第一次循环执行完了
- 然后执行第二次循环 F一1二2 所以和和上面一样添加3 添加 2 把track添加到res中然后踢出 2 3
那么第一层循环的第一次循环中的第一层递归也就结束了 然后向下执行把1踢出track - 然后就是执行第一层循环的第二,三次循环 同第一次循环就不做赘述了
测试代码(c++):
头文件:
#pragma once
//回溯算法就是数的遍历 循环实现广度的遍历 递归调用实现深度的遍历
#include<iostream>
#include<vector>
#include<deque>
using namespace std;
//template<class T>
vector<vector<int>>res;
//template<class T>
void trackBack( vector<int>nums, vector<int>& flags, vector<int>& track)//这三个参数分别是要遍历的数组 作为标记的bool数组 一次广度遍历的结果 最后返回得到的二维数组
{
//判断一次排列是否已经完成
if (track.size() == nums.size())
{
res.push_back(track);//一次深度遍历等到一条路径添加到结果集中
return ;
}
for (int i = 0; i <nums.size(); i++)
{
if (flags[i])
continue;
track.push_back(nums[i]);//添加元素到路径中
flags[i] = 1;//标记
trackBack(nums, flags, track);//递归调用回溯函数
track.pop_back();//退格元素
flags[i] = 0;//还原标记
}
}
源文件:*(测试文件)
#include"trackBack.h"
#include<vector>
#include<deque>
using namespace std;
int main()
{
vector<int>nums;//输入的元素
vector<int>track;//一次排列的元素集合
vector<int>flags;
int n = 0;
cout << "请输入您要全排列的元素的个数:";
cin >> n;
int elem;
cout << "请输入" << n << "个元素:";
for (int i = 0; i < n; i++)
{
cin >> elem;
nums.push_back(elem);
flags.push_back(0);
}
trackBack(nums, flags, track);
cout << "全排列为:" << endl;
for (int i =0 ; i < res.size(); i++)
{
for (int j = 0; j < res[i].size(); j++)
cout << res[i][j] << " ";
cout << endl;
}
system("pause");
return 0;
}
运行结果: