1.什么是回溯算法?

  • 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

回溯算法解题套路模板:

  • 1.回溯出口,当找到了一个问题的解时,存储该解。
  • 2.回溯主体,就是遍历当前的状态的所有子节点,并判断下一个状态是否是满足问题条件的,如果满足问题条件,那么进入下一个状态。
  • 3.状态返回,如果当前状态不满足条件,那么返回到前一个状态。
res = []    # 定义全局变量保存最终结果
state = []  # 定义状态变量保存当前状态
p,q,r       # 定义条件变量(一般条件变量就是题目直接给的参数)
def back(状态,条件1,条件2,……):if # 不满足合法条件(可以说是剪枝)returnelif # 状态满足最终要求res.append(state)   # 加入结果return # 主要递归过程,一般是带有 循环体 或者 条件体for # 满足执行条件if  # 满足执行条件back(状态,条件1,条件2,……)
back(状态,条件1,条件2,……)
return res
  • 简化版
public void backtrack(...){if(是一个可行解) 将结果存入集合中;if(无备选项或无须进一步搜索) return;for(所有的备选项){if(该备选项不满足约束) continue;生成当前节点;更新集合;子问题backtrack();//状态回退删除节点;回退集合;}
}

实战出真知,下面请看例题:
例1:
46. 全排列

  • 1.出口:当temp列表的大小和给定数组的长度一致时,说明形成了一个可行结果,需要存入ret中。同时两者数值大小一致,也说明再无备选项,搜索应该回溯到上一步。
  • 2.主体: 筛选满足约束的备选项。这里用到了一个布尔数组uesd,用来记录哪些数是已经被使用了的。显然我们应该选取那些未被使用过的数。布尔数组的技巧非常实用,应该记住。
  • 3.返回: 产生子问题并求解,求解完后恢复求解之前的状态。子问题求解前需要更新布尔数组used和暂存列表temp,子问题求解完以后,需要恢复used和temp之前的状态。
#include <iostream>
#include <vector>
using namespace std;
class Solution {public:vector<vector<int>> permute(vector<int>& nums){vector<vector<int>> ret;vector<bool> used(nums.size(), false);backTrack(nums, ret,new vector<int>,used);return ret;}void backTrack(vector<int>& nums, vector<vector<int>>& ret, vector<int>* temp, vector<bool>& used){//当temp数组存储元素个数等于size时,说明找到了一个解if (temp->size() == nums.size()){ret.push_back(*temp);}else{//遍历所有可能子节点for (int i = 0; i < nums.size(); i++){//如果当前子节点已经被访问过,那么就跳过该子节点,访问下一个子节点if (used[i]== true)continue;    //设置当前没有访问过的子节点为访问过used[i] = true;//并加入temp数组temp->push_back(nums[i]);//temp的数组个数还不等于size,无法构成一个完整的解,因此需要通过递归找到剩余解backTrack(nums,ret,temp, used);//进行回退操作,将当前子节点的访问性改成未被访问过used[i] = false;//删除temp数组尾部元素,弹出当前子节点,因为当前子节点不符合规则temp->pop_back();}}}
};
void test()
{Solution s;vector<int> nums = { 1,2,3 };vector<vector<int>> ret=s.permute(nums);for (int i = 0; i < ret.size(); i++){for (int j = 0; j < ret[0].size(); j++){cout << ret[i][j] << " ";}cout << endl;}
}
int main()
{          test();system("pause");return 0;
}



官方解法:


  • 要排数组分两半,左排过,右没排
  • 通过交换,来得到不同的排列组合
class Solution {public:void backtrack(vector<vector<int>>& res, vector<int>& output, int first, int len){// 所有数都填完了if (first == len) {res.emplace_back(output);return;}for (int i = first; i < len; ++i) {// 动态维护数组swap(output[i], output[first]);// 继续递归填下一个数backtrack(res, output, first + 1, len);// 撤销操作swap(output[i], output[first]);}}vector<vector<int>> permute(vector<int>& nums) {vector<vector<int> > res;backtrack(res, nums, 0, (int)nums.size());return res;}
};


例2:
47. 全排列 II

相较于上一题全排列,这题最大的难点在于如何解决重复数字的排列问题
如果继续采用上一题的解法,在输入为[1,1,2]的情况下,[2,1,1]这种结果会出现2次,显然不满足题目要求。
这里我们不妨回顾一下排列公式和组合公式。A(3,4) = 432,而C(3,4) = 432/(321),两者的唯一区别在于,组合去除了排列的顺序,即组合不在乎所选3个数字的具体选择顺序是怎么样的,只在乎我们具体选择了哪3个数字。
进一步,若给排列问题加上数字选择的顺序约束,即只能按照编号从小到大选择数字,其实质也将变为组合问题。
回到原问题,我们能否也去除数组中这两个1的顺序性,从而避免产生重复情况。类似的,我们将固定2个1的选择顺序,只能先选择第1个1,再选择第2个1,而不存在先选择第2个1,再选择第1个1的情况。
当然在调用回溯算法以前,我们需要先对数组进行排序从而确保相同的数是相邻的。除此之外,和上一题的唯一差别仅在于选取备选项时要求固定顺序。

class Solution {public:vector<vector<int>> permuteUnique(vector<int>& nums) {vector<vector<int>> ret;vector<bool> used(nums.size(), false);vector<int> path;sort(nums.begin(), nums.end());backTrack(nums, ret,path,used);return ret;}void backTrack(vector<int>& nums, vector<vector<int>>& ret, vector<int>& path, vector<bool>& used){if (path.size() == nums.size()){ret.push_back(path);return;}else{for (int i = 0; i < nums.size(); i++){if (used[i] || (i>0&&nums[i] == nums[i-1] && !used[i-1]))continue;used[i] = true;path.push_back(nums[i]);backTrack(nums, ret, path, used);used[i] = false;path.pop_back();}}}
};


例3:
39. 组合总和

  • 1.出口:当所选取数字和为目标值时
  • 2.主体:每一次都从数组首元素开始选择一个元素,如果最后发现累加发现无法构成目标值,就回退从第二个元素试水,依次类推
  • 3.返回:构不成目标值或者构成了目标值还需要继续寻找其他解,都需要回退剪
    枝来寻找其他可能解

注意:

这题同样存在如何避免重复结果的问题。类似的,为了变排列问题为组合问题,我们对数组的全部数字固定了选择顺序——下标小的数字必须在下标大的数字之前被选择。这里不再用到布尔数组used,原因是当我们固定了选择顺序后,在每一步,我们都容易知道,下标大于当前数字的是未被选择过的,而下标小于当前数字的是已经被选择过的。
不能再次选择之前选择过的数字,当选过第二个数字后,就不能选择第一个数字了

class Solution {public:vector<vector<int>> combinationSum(vector<int>& candidates, int target){vector<vector<int>> ret;vector<int> path;backTrack(ret, target, path, candidates,0);return ret;}//这里left作用:不能再次选择之前选择过的数字void backTrack(vector<vector<int>>& ret, int target, vector<int>& path, vector<int>& cand, int left){if (accumulate(path.begin(), path.end(), 0) == target){ret.push_back(path);return;}else if (accumulate(path.begin(), path.end(), 0) > target){return;}else{for (int i = left; i < cand.size(); i++){path.push_back(cand[i]);backTrack(ret, target, path, cand, i);path.pop_back();}}}
};


例4:走迷宫

//迷宫 1为墙 0为通路
int Graph[][6] =
{{1, 1, 1, 1, 1, 1},{1, 0, 0, 0, 1, 1},{1, 0, 1, 0, 0, 1},{1, 0, 0, 0, 1 ,1},{1, 1, 0, 0, 0, 1},{1, 1, 1, 1, 1, 1}
};
  • 打印输出所有通路

出口:找到了迷宫出口
主体:从起点开始,不断往四周深入探索,直到遇到最终出口
返回:如果找到了一条通路或者进入思路,那么需要往后退一步,寻找其他可能解

#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
//迷宫 1为墙 0为通路
int Graph[][6] =
{{1, 1, 1, 1, 1, 1},{1, 0, 0, 0, 1, 1},{1, 0, 1, 0, 0, 1},{1, 0, 0, 0, 1 ,1},{1, 1, 0, 0, 0, 1},{1, 1, 1, 1, 1, 1}
};
//迷宫 起点 终点
void findPath(int(*Graph)[6], int x1, int y1, int x2, int y2)
{//存放迷宫路径static vector<pair<int,int>> path;//找到出口if (x1 == x2 && y1 == y2){cout << "迷宫路径如下:" << endl;path.push_back({ x1,y1 });Graph[x1][y1] = -1;for_each(path.begin(), path.end(), [](pair<int, int> path) {cout << "{" << path.first << "," << path.second << "}" << endl; });return;}else{if (Graph[x1][y1]==0)//当前要走的方向为通路{int di = 0;//方向--右 下 左 上int x, y;//记录下一次要走的方向for (; di < 4; di++){switch (di){case 0://右---列加一{x = x1;y = y1 + 1;break;}case 1://下---行加一{x = x1 + 1;y = y1;break;}case 2://左---列减一{x = x1;y = y1 - 1;break;}case 3://上---行减一{x = x1 - 1;y = y1;break;}}path.push_back({ x1,y1 });Graph[x1][y1] = -1;findPath(Graph, x, y, x2, y2);//更新寻找的起点//回溯加剪枝Graph[x1][y1] = 0;path.pop_back();}}}
}
int main()
{findPath(Graph, 1, 1, 4, 4);system("pause");return 0;
}

总结:

  • 现在我们对回溯算法进行总结。回溯算法的本质是一种深搜递归遍历。通过上面的例题,我们可以知道回溯算法可以对各类组合、排列问题进行较好的求解,所以当遇到可以抽象建模为排列或组合的问题,回溯算法都可以作为一种求解手段。

小白易懂的回溯算法!!!相关推荐

  1. 人工智能实现a*算法解决八数码_小白带你学回溯算法

    微信公众号:小白算法 关注可了解更多算法,并能领取免费资料.问题或建议,请公众号留言;小白算法,简单白话算法,每个人都能看懂的算法 上一期算法回顾--贪婪法:https://mp.weixin.qq. ...

  2. 《计算机视觉与图像处理》最全总结之就业必备-小白易懂易上手

    <计算机视觉与图像处理>最全总结之就业必备-小白易懂易上手 最近需要找工作找实习,自己就把计算机视觉方向的知识点重新梳理了一遍,看的是开课吧CV视频资料,并写了一些笔记,需要的话可转场获取 ...

  3. 常用十大算法 非递归二分查找、分治法、动态规划、贪心算法、回溯算法(骑士周游为例)、KMP、最小生成树算法:Prim、Kruskal、最短路径算法:Dijkstra、Floyd。

    十大算法 学完数据结构该学什么?当然是来巩固算法,下面介绍了十中比较常用的算法,希望能帮到大家. 包括:非递归二分查找.分治法.动态规划.贪心算法.回溯算法(骑士周游为例).KMP.最小生成树算法:P ...

  4. 回溯算法(以解决n皇后问题为例)

    基本思想: 回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试.八皇后问题就是回溯算法的典型,第一步按照顺序放一个皇后,然后第二步符合要求放第2个皇后,如果没有位置符合要求, ...

  5. 回溯算法背包问题(java实现)

    一.回溯算法 回溯算法,建议先看视频,b站搜索与回溯算法1 最经典的题就是全排列问题 从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列.当m= ...

  6. 通用解题法——回溯算法(理解+练习)

    积累算法经验,积累解题方法--回溯算法,你必须要掌握的解题方法! 什么是回溯算法呢? 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就&quo ...

  7. 用栈、回溯算法设计迷宫程序

    目录 1.走迷宫与回溯算法 2.迷宫设计栈扮演的角色 3.Python实现走迷宫 栈的应用有许多,本篇博文着重将栈与回溯(Backtracking)算法结合,设计走迷宫程序.其实回溯算法也是人工智能的 ...

  8. 数字拆分问题算法回溯_回溯算法:求子集问题!

    给「代码随想录」一个星标吧! ❝ 认识本质之后,这就是一道模板题 通知:我将公众号文章和学习相关的资料整理到了Github :https://github.com/youngyangyang04/le ...

  9. C语言回溯算法解决N皇后问题

    回溯算法的模型是 x++, not satisfy ? x-- : continue. 代码中x作列号,y[x]保存第x列上皇后放置的位置. 1 #include<stdio.h> 2 # ...

最新文章

  1. IDEA打包成war部署web项目
  2. 【跃迁之路】【448天】刻意练习系列207(2018.04.29)
  3. c语言 结构数组读取,有关文件读写到结构体数组问题;
  4. 玩转 SpringBoot 2.x 之自定义Starter依赖
  5. Acrobat Pro DC 教程,如何编辑 PDF 文件中的文本和图片?
  6. 制作uefi的kali安装盘_制作 UD + EFI + PE + KALI 启动U盘
  7. 01-【分析】神策系统的拆解与分析
  8. oppor17刷鸿蒙系统,oppo a5刷机包下载
  9. 《绝区零》首测遭破解,面临资源泄露风险
  10. 1 Apache启动失败,请检查相关配置 √MySQL5 1已启动 解决方案
  11. iOS动态库重签名步骤
  12. iOS 跳转淘宝、天猫、京东商品详情页
  13. NeurIPS 2020 | 基于协同集成与分发的协同显著性目标检测网络
  14. CPP】【const 指针与引用】实验3实验内容5
  15. 农夫过河问题(一个有趣的问题-位运算)
  16. 基于python的视频监控系统_Python远程视频监控程序
  17. 第一个php小程序(学习)
  18. 一文看懂膨胀(空洞)卷积(含代码)
  19. Benchmark初印象
  20. 2021年西雅图第一家通过传统IPO上市的公司

热门文章

  1. 大学美术报计算机一级,美术报
  2. 论文笔记-Self-Supervised Monocular Depth Hints
  3. 神奇的裸女图片(转)
  4. 别人的研究生宿舍男女混住,而我没地方住?
  5. 伪原创视频怎么做 视频md5修改批量修改器
  6. Linux 通配符与三种引号
  7. 如何office Excel 设置取消每次打开文件的外部链接提醒,并且不更新
  8. 从拳头公司的CG动画讲起:如何让你的动画更有“电影感”
  9. 区块链分叉如何解决_聊聊区块链背后存在的问题(一):分叉
  10. 四边形不等式技巧——分割数组的最大值(画家问题)