一. 回溯法 – 深度优先搜素

1. 简单概述

回溯法思路的简单描述是:把问题的解空间转化成了图或者树的结构表示,然后使用深度优先搜索策略进行遍历,遍历的过程中记录和寻找所有可行解或者最优解。

基本思想类同于:

  • 图的深度优先搜索
  • 二叉树的后序遍历

分支限界法:广度优先搜索

思想类同于:图的广度优先遍历

二叉树的层序遍历

2. 详细描述

详细的描述则为:

回溯法按深度优先策略搜索问题的解空间树。首先从根节点出发搜索解空间树,当算法搜索至解空间树的某一节点时,先利用剪枝函数判断该节点是否可行(即能得到问题的解)。如果不可行,则跳过对该节点为根的子树的搜索,逐层向其祖先节点回溯;否则,进入该子树,继续按深度优先策略搜索。

回溯法的基本行为是搜索,搜索过程使用剪枝函数来为了避免无效的搜索。剪枝函数包括两类:1. 使用约束函数,剪去不满足约束条件的路径;2.使用限界函数,剪去不能得到最优解的路径。

问题的关键在于如何定义问题的解空间,转化成树(即解空间树)。解空间树分为两种:子集树和排列树。两种在算法结构和思路上大体相同。

3. 回溯法应用

当问题是要求满足某种性质(约束条件)的所有解或最优解时,往往使用回溯法。

它有“通用解题法”之美誉。

二. 回溯法实现 - 递归和递推(迭代)

回溯法的实现方法有两种:递归和递推(也称迭代)。一般来说,一个问题两种方法都可以实现,只是在算法效率和设计复杂度上有区别。
      【类比于图深度遍历的递归实现和非递归(递推)实现】

1. 递归

思路简单,设计容易,但效率低,其设计范式如下:

[cpp] view plain copy

  1. //针对N叉树的递归回溯方法
  2. void backtrack (int t)
  3. {
  4. if (t>n) output(x); //叶子节点,输出结果,x是可行解
  5. else
  6. for i = 1 to k//当前节点的所有子节点
  7. {
  8. x[t]=value(i); //每个子节点的值赋值给x
  9. //满足约束条件和限界条件
  10. if (constraint(t)&&bound(t))
  11. backtrack(t+1);  //递归下一层
  12. }
  13. }

2. 递推

算法设计相对复杂,但效率高。

[cpp] view plain copy

  1. //针对N叉树的迭代回溯方法
  2. void iterativeBacktrack ()
  3. {
  4. int t=1;
  5. while (t>0) {
  6. if(ExistSubNode(t)) //当前节点的存在子节点
  7. {
  8. for i = 1 to k  //遍历当前节点的所有子节点
  9. {
  10. x[t]=value(i);//每个子节点的值赋值给x
  11. if (constraint(t)&&bound(t))//满足约束条件和限界条件
  12. {
  13. //solution表示在节点t处得到了一个解
  14. if (solution(t)) output(x);//得到问题的一个可行解,输出
  15. else t++;//没有得到解,继续向下搜索
  16. }
  17. }
  18. }
  19. else //不存在子节点,返回上一层
  20. {
  21. t--;
  22. }
  23. }
  24. }

三. 子集树和排列树

1. 子集树

所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间成为子集树。
如0-1背包问题,从所给重量、价值不同的物品中挑选几个物品放入背包,使得在满足背包不超重的情况下,背包内物品价值最大。它的解空间就是一个典型的子集树。

回溯法搜索子集树的算法范式如下:

[cpp] view plain copy

  1. void backtrack (int t)
  2. {
  3. if (t>n) output(x);
  4. else
  5. for (int i=0;i<=1;i++) {
  6. x[t]=i;
  7. if (constraint(t)&&bound(t)) backtrack(t+1);
  8. }
  9. }

2. 排列树

所给的问题是确定n个元素满足某种性质的排列时,相应的解空间就是排列树。
如旅行售货员问题,一个售货员把几个城市旅行一遍,要求走的路程最小。它的解就是几个城市的排列,解空间就是排列树。
      回溯法搜索排列树的算法范式如下:

[cpp] view plain copy

  1. void backtrack (int t)
  2. {
  3. if (t>n) output(x);
  4. else
  5. for (int i=t;i<=n;i++) {
  6. swap(x[t], x[i]);
  7. if (constraint(t)&&bound(t)) backtrack(t+1);
  8. swap(x[t], x[i]);
  9. }
  10. }

四. 经典问题

(1)装载问题
(2)0-1背包问题
(3)旅行售货员问题
(4)八皇后问题
(5)迷宫问题
(6)图的m着色问题

1. 0-1背包问题

问题:给定n种物品和一背包。物品i的重量是wi,其价值为pi,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
        分析:问题是n个物品中选择部分物品,可知,问题的解空间是子集树。比如物品数目n=3时,其解空间树如下图,边为1代表选择该物品,边为0代表不选择该物品。使用x[i]表示物品i是否放入背包,x[i]=0表示不放,x[i]=1表示放入。回溯搜索过程,如果来到了叶子节点,表示一条搜索路径结束,如果该路径上存在更优的解,则保存下来。如果不是叶子节点,是中点的节点(如B),就遍历其子节点(D和E),如果子节点满足剪枝条件,就继续回溯搜索子节点。

代码:

[cpp] view plain copy

  1. #include <stdio.h>
  2. #define N 3         //物品的数量
  3. #define C 16        //背包的容量
  4. int w[N]={10,8,5};  //每个物品的重量
  5. int v[N]={5,4,1};   //每个物品的价值
  6. int x[N]={0,0,0};   //x[i]=1代表物品i放入背包,0代表不放入
  7. int CurWeight = 0;  //当前放入背包的物品总重量
  8. int CurValue = 0;   //当前放入背包的物品总价值
  9. int BestValue = 0;  //最优值;当前的最大价值,初始化为0
  10. int BestX[N];       //最优解;BestX[i]=1代表物品i放入背包,0代表不放入
  11. //t = 0 to N-1
  12. void backtrack(int t)
  13. {
  14. //叶子节点,输出结果
  15. if(t>N-1)
  16. {
  17. //如果找到了一个更优的解
  18. if(CurValue>BestValue)
  19. {
  20. //保存更优的值和解
  21. BestValue = CurValue;
  22. for(int i=0;i<N;++i) BestX[i] = x[i];
  23. }
  24. }
  25. else
  26. {
  27. //遍历当前节点的子节点:0 不放入背包,1放入背包
  28. for(int i=0;i<=1;++i)
  29. {
  30. x[t]=i;
  31. if(i==0) //不放入背包
  32. {
  33. backtrack(t+1);
  34. }
  35. else //放入背包
  36. {
  37. //约束条件:放的下
  38. if((CurWeight+w[t])<=C)
  39. {
  40. CurWeight += w[t];
  41. CurValue += v[t];
  42. backtrack(t+1);
  43. CurWeight -= w[t];
  44. CurValue -= v[t];
  45. }
  46. }
  47. }
  48. //PS:上述代码为了更符合递归回溯的范式,并不够简洁
  49. }
  50. }
  51. int main(int argc, char* argv[])
  52. {
  53. backtrack(0);
  54. printf("最优值:%d\n",BestValue);
  55. for(int i=0;i<N;i++)
  56. {
  57. printf("最优解:%-3d",BestX[i]);
  58. }
  59. return 0;
  60. }

2. 旅行售货员问题

回溯法----旅行售货员问题

3. 详细描述N皇后问题

问题:在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

N皇后问题等价于在n×n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。

分析:从n×n个格子中选择n个格子摆放皇后。可见解空间树为子集树。

使用Board[N][N]来表示棋盘,Board[i][j]=0 表示(I,j)位置为空,Board[i][j]=1 表示(I,j)位置摆放有一个皇后。

全局变量way表示总共的摆放方法数目。

使用Queen(t)来摆放第t个皇后。Queen(t) 函数符合子集树时的递归回溯范式。当t>N时,说明所有皇后都已经摆   放完成,这是一个可行的摆放方法,输出结果;否则,遍历棋盘,找皇后t所有可行的摆放位置,Feasible(i,j) 判断皇后t能否摆放在位置(i,j)处,如果可以摆放则继续递归摆放皇后t+1,如果不能摆放,则判断下一个位置。

Feasible(row,col)函数首先判断位置(row,col)是否合法,继而判断(row,col)处是否已有皇后,有则冲突,返回0,无则继续判断行、列、斜方向是否冲突。斜方向分为左上角、左下角、右上角、右下角四个方向,每次从(row,col)向四个方向延伸一个格子,判断是否冲突。如果所有方向都没有冲突,则返回1,表示此位置可以摆放一个皇后。

代码:

[cpp] view plain copy

  1. /************************************************************************
  2. * 名  称:NQueen.cpp
  3. * 功  能:回溯算法实例:N皇后问题
  4. * 作  者:JarvisChu
  5. * 时  间:2013-11-13
  6. ************************************************************************/
  7. #include <stdio.h>
  8. #define N 8
  9. int Board[N][N];//棋盘 0表示空白 1表示有皇后
  10. int way;//摆放的方法数
  11. //判断能否在(x,y)的位置摆放一个皇后;0不可以,1可以
  12. int Feasible(int row,int col)
  13. {
  14. //位置不合法
  15. if(row>N || row<0 || col >N || col<0)
  16. return 0;
  17. //该位置已经有皇后了,不能
  18. if(Board[row][col] != 0)
  19. {   //在行列冲突判断中也包含了该判断,单独提出来为了提高效率
  20. return 0;
  21. }
  22. //
  23. //下面判断是否和已有的冲突
  24. //行和列是否冲突
  25. for(int i=0;i<N;++i)
  26. {
  27. if(Board[row][i] != 0 || Board[i][col]!=0)
  28. return 0;
  29. }
  30. //斜线方向冲突
  31. for(int i=1;i<N;++i)
  32. {
  33. /* i表示从当前点(row,col)向四个斜方向扩展的长度
  34. 左上角 \  / 右上角   i=2
  35. \/           i=1
  36. /\           i=1
  37. 左下角 /  \ 右下角   i=2
  38. */
  39. //左上角
  40. if((row-i)>=0 && (col-i)>=0)    //位置合法
  41. {
  42. if(Board[row-i][col-i] != 0)//此处已有皇后,冲突
  43. return 0;
  44. }
  45. //左下角
  46. if((row+i)<N && (col-i)>=0)
  47. {
  48. if(Board[row+i][col-i] != 0)
  49. return 0;
  50. }
  51. //右上角
  52. if((row-i)>=0 && (col+i)<N)
  53. {
  54. if(Board[row-i][col+i] != 0)
  55. return 0;
  56. }
  57. //右下角
  58. if((row+i)<N && (col+i)<N)
  59. {
  60. if(Board[row+i][col+i] != 0)
  61. return 0;
  62. }
  63. }
  64. return 1; //不会发生冲突,返回1
  65. }
  66. //摆放第t个皇后 ;从1开始
  67. void Queen(int t)
  68. {
  69. //摆放完成,输出结果
  70. if(t>N)
  71. {
  72. way++;
  73. /*如果N较大,输出结果会很慢;N较小时,可以用下面代码输出结果
  74. for(int i=0;i<N;++i){
  75. for(int j=0;j<N;++j)
  76. printf("%-3d",Board[i][j]);
  77. printf("\n");
  78. }
  79. printf("\n------------------------\n\n");
  80. */
  81. }
  82. else
  83. {
  84. for(int i=0;i<N;++i)
  85. {
  86. for(int j=0;j<N;++j)
  87. {
  88. //(i,j)位置可以摆放皇后,不冲突
  89. if(Feasible(i,j))
  90. {
  91. Board[i][j] = 1;  //摆放皇后t
  92. Queen(t+1);       //递归摆放皇后t+1
  93. Board[i][j] = 0;  //恢复
  94. }
  95. }
  96. }
  97. }
  98. }
  99. //返回num的阶乘,num!
  100. int factorial(int num)
  101. {
  102. if(num==0 || num==1)
  103. return 1;
  104. return num*factorial(num-1);
  105. }
  106. int main(int argc, char* argv[])
  107. {
  108. //初始化
  109. for(int i=0;i<N;++i)
  110. {
  111. for(int j=0;j<N;++j)
  112. {
  113. Board[i][j]=0;
  114. }
  115. }
  116. way = 0;
  117. Queen(1);  //从第1个皇后开始摆放
  118. //如果每个皇后都不同
  119. printf("考虑每个皇后都不同,摆放方法:%d\n",way);//N=8时, way=3709440 种
  120. //如果每个皇后都一样,那么需要除以 N!出去重复的答案(因为相同,则每个皇后可任意调换位置)
  121. printf("考虑每个皇后都不同,摆放方法:%d\n",way/factorial(N));//N=8时, way=3709440/8! = 92种
  122. return 0;
  123. }

PS:该问题还有更优的解法。充分利用问题隐藏的约束条件:每个皇后必然在不同的行(列),每个行(列)必然也只有一个皇后。这样我们就可以把N个皇后放到N个行中,使用Pos[i]表示皇后i在i行中的位置(也就是列号)(i = 0 to N-1)。这样代码会大大的简洁,因为节点的子节点数目会减少,判断冲突也更简单。

4. 迷宫问题

问题:给定一个迷宫,找到从入口到出口的所有可行路径,并给出其中最短的路径

分析:用二维数组来表示迷宫,则走迷宫问题用回溯法解决的的思想类似于图的深度遍历。从入口开始,选择下一个可以走的位置,如果位置可走,则继续往前,如果位置不可走,则返回上一个位置,重新选择另一个位置作为下一步位置。

N表示迷宫的大小,使用Maze[N][N]表示迷宫,值为0表示通道(可走),值为1表示不可走(墙或者已走过);

Point结构体用来记录路径中每一步的坐标(x,y)

(ENTER_X,ENTER_Y) 是迷宫入口的坐标

(EXIT_X, EXIT _Y)    是迷宫出口的坐标

Path容器用来存放一条从入口到出口的通路路径

BestPath用来存放所有路径中最短的那条路径

Maze()函数用来递归走迷宫,具体步骤为:

1. 首先将当前点加入路径,并设置为已走
       2. 判断当前点是否为出口,是则输出路径,保存结果;跳转到4
       3. 依次判断当前点的上、下、左、右四个点是否可走,如果可走则递归走该点
       4. 当前点推出路径,设置为可走

代码:

[cpp] view plain copy

  1. /************************************************************************
  2. * 名  称:Maze.cpp
  3. * 功  能:回溯算法实例:迷宫问题
  4. * 作  者:JarvisChu
  5. * 时  间:2013-11-13
  6. ************************************************************************/
  7. #include <iostream>
  8. #include <vector>
  9. using namespace std;
  10. typedef struct
  11. {
  12. int x;
  13. int y;
  14. }Point;
  15. #define N 10         //迷宫的大小
  16. #define ENTER_X 0    //入口的位置(0,0)
  17. #define ENTER_Y 0
  18. #define EXIT_X N-1   //出口的位置(N-1,N-1)
  19. #define EXIT_Y N-1
  20. int Maze[N][N];<span style="white-space:pre;">      </span>//定义一个迷宫,0表示通道,1表示不可走(墙或已走)
  21. int paths;<span style="white-space:pre;">       </span>//路径条数
  22. vector<Point> Path;<span style="white-space:pre;">    </span>//保存一条可通的路径
  23. vector<Point> BestPath;<span style="white-space:pre;">    </span>//保存最短的路径
  24. bool First = true;<span style="white-space:pre;">   </span>//标志,找到第一条路径
  25. //初始化迷宫
  26. void InitMaze()
  27. {
  28. <span style="white-space:pre;"> </span>//简单起见,本题定义一个固定大小10*10的迷宫
  29. <span style="white-space:pre;"> </span>//定义一个迷宫,0表示通道,1表示墙(或不可走)
  30. int mz[10][10]={
  31. {0,0,1,1,1,1,1,1,1,1}, //0
  32. {1,0,0,1,1,0,0,1,0,1}, //1
  33. {1,0,0,1,0,0,0,1,0,1}, //2
  34. {1,0,0,0,0,1,1,0,0,1}, //3
  35. {1,0,1,1,1,0,0,0,0,1}, //4
  36. {1,0,0,0,1,0,0,0,0,1}, //5
  37. {1,0,1,0,0,0,1,0,0,1}, //6
  38. {1,0,1,1,1,0,1,1,0,1}, //7
  39. {1,1,0,0,0,0,0,0,0,0}, //8
  40. {1,1,1,1,1,1,1,1,1,0}  //9
  41. //   0 1 2 3 4 5 6 7 8 9
  42. };
  43. //复制到迷宫
  44. memcpy(Maze,mz,sizeof(mz));
  45. paths = 0;
  46. }
  47. //从(x,y)位置开始走;初始为(0,0)
  48. void MazeTrack(int x,int y)
  49. {
  50. ///
  51. //当前点加入到路径
  52. Point p={x,y};
  53. Path.push_back(p);
  54. Maze[x][y] = 1;         //设置为已走,不可走
  55. //cout<<"来到("<<x<<","<<y<<")"<<endl;
  56. ///
  57. //如果该位置是出口,输出结果
  58. if(x == EXIT_X && y== EXIT_Y)
  59. {
  60. cout<<"找到一条道路"<<endl;
  61. paths++;
  62. //输出路径
  63. vector<Point>::iterator it;
  64. for(it=Path.begin();it!=Path.end();++it)
  65. {
  66. cout<<"("<<it->x<<","<<it->y<<") ";
  67. }
  68. cout<<endl;
  69. //判断是否更优
  70. if(First)//如果是找到的第一条路径,直接复制到最优路径
  71. {
  72. for(it=Path.begin();it!=Path.end();++it)
  73. {
  74. BestPath.push_back(*it);
  75. }
  76. First = false;
  77. }
  78. else //不是第一条,则判断是否更短
  79. {

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/weixin_43556670/article/details/105567783

五大常用算法之回溯法详解及经典例题相关推荐

  1. [回溯算法] 五大常用算法之回溯法

    算法入门6:回溯法 一. 回溯法 – 深度优先搜素 1. 简单概述 回溯法思路的简单描述是:把问题的解空间转化成了图或者树的结构表示,然后使用深度优先搜索策略进行遍历,遍历的过程中记录和寻找所有可行解 ...

  2. 五大常用算法一(回溯,随机化,动态规划)

    五大常用算法一(回溯,随机化,动态规划) 回溯算法 回溯法: 也称为试探法,它并不考虑问题规模的大小,而是从问题的最明显的最小规模开始逐步求解出可能的答案,并以此慢慢地扩大问题规模,迭代地逼近最终问题 ...

  3. 五大常用算法——回溯算法详解及经典例题

    回溯算法 1.回溯算法就是一种有组织的系统最优化搜索技术,可以看作蛮力法穷举搜索的改进.回溯法常常可以避免搜索所有可能的解,所以它适用于求解组织数量较大的问题. 2.首先我们先了解一下一个基本概念&q ...

  4. 五大常用算法——分支限界算法详解及经典例题

    一.对比回溯法 回溯法的求解目标是找出解空间中满足约束条件的所有解,想必之下,分支限界法的求解目标则是找出满足约束条件的一个解,或是满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意 ...

  5. LV算法和回溯法结合解n后问题

    我的算法作业:n=12 ~ 20时,求出较优的stopVegas值.(也就随机放置几个皇后比较好的问题). 解答:(VC6.0下调试通过,可能某些统计数据的计算部分,有不太妥当的地方,贴出来也就是有高 ...

  6. 批处理作业调度问题 ——回溯法详解

    1.问题描述 每一个作业Ji都有两项任务分别在2台机器上完成.每个作业必须先有机器1处理,然后再由机器2处理.作业Ji需要机器j的处理时间为tji.对于一个确定的作业调度,设Fji是作业i在机器j上完 ...

  7. 八皇后问题、N皇后问题回溯法详解

    /* * 回溯法解N皇后问题 * 使用一个一维数组表示皇后的位置 * 其中数组的下标表示皇后所在的行 * 数组元素的值表示皇后所在的列 * 这样设计的棋盘,所有皇后必定不在同一行 * * 假设前n-1 ...

  8. 五大常用算法——动态规划算法详解及经典例题

    一.基本概念 动态规划是运筹学中用于求解决策过程中的最优化数学方法.当然,我们在这里关注的是作为一种算法设计技术,作为一种使用多阶段决策过程最优的通用方法. 动态规划过程是:每次决策依赖于当前状态,又 ...

  9. 五大常用算法——分治算法详解及经典例题

    一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是"分而治之",就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题--直到最后子 ...

最新文章

  1. 拷贝eclipse 工作空间 workspace 步骤
  2. 【Android 安装包优化】Android 应用中 7zr 可执行程序准备 ( Android Studio 导入可执行 7zr 程序 | 从 Assets 资源文件拷贝 7zr 到内置存储 )
  3. mysql 经典优化案例_MySQL-SQL优化10大最经典案例详解
  4. jQuery 3.3.1已经发布,开发团队正在准备4.0版本
  5. linux树莓派 ssh密码,树莓派之SSH连接经验
  6. commons-pool2-2.3 jar包_[漏洞复现]FastJson 1.2.61远程代码执行漏洞(From第三方jar包)
  7. 小米台灯底座接口很松_选购台灯发愁?这篇桌面照明方案助你一臂之力
  8. aspose word 删除空行_Word:删除空行
  9. IOS开发之-人脸识别
  10. 模拟银行排队叫号机 2011.04.18
  11. npm方法创建一个vue项目,引入element插件
  12. 人脸聚类Learning to Cluster Faces(翻译)
  13. [note] 电磁场和微波课组(一)——电磁学(电学部分)
  14. 12v继电器驱动电路
  15. git error: You have not concluded your merge (MERGE_HEAD exists).
  16. CK11N改标准价格
  17. 图像sensor的工作原理
  18. FutureWarning: The default value of numeric_only in DataFrame.mean is deprecated.
  19. uni-app 上传图片到七牛云
  20. 苹果cms首页文件html,苹果cms安装完不显示首页的解决方法

热门文章

  1. Android实现点击跳转本地浏览器打开网页的精简方式
  2. 2020起重机械指挥模拟考试题及起重机械指挥作业考试题库
  3. 智能灌溉APP开发如何解决用户问题--甲由科技为你支招
  4. 物理CPU,CPU核数,逻辑CPU
  5. linux c编程获取excel文件内容,c读取excel文件内容
  6. 实验吧/隐写术/小苹果
  7. JavaScript中如何比较两个日期(记录)
  8. IntelliJ IDEA 恢复出厂设置
  9. PostgreSQL操作-psql命令详解
  10. 常见的浏览器兼容性问题和解决方法