回溯法及举例分析

系统管理员 2022-05-16 08:19 227阅读 0赞

回溯法,按照百度百科的介绍,是指一种选优的搜索法,按照选优条件向前搜索,以达到目标,但当搜索到某一步时发现原先的选择并不优或者不能达到目标,则退回上一步重新选择,这种走不通就退回重新选择再走的方法就是回溯法。

可用回溯法求解的问题P一般可以被如下描述:对于已知的由n元组(x1,x2,…,xn)组成的一个状态空间E={(x1,x2,…,xn)∣xi∈Si ,i=1,2,…,n},给定关于n元组中的一个分量的一个约束集D,要求E中满足D的全部约束条件的所有n元组。其中Si是分量xi的定义域,且 |Si| 有限,i=1,2,…,n。我们称E中满足D的全部约束条件的任一n元组为问题P的一个解。

对于许多问题,所给定的约束集D具有完备性,即i元组(x1,x2,…,xi)满足D中仅涉及到x1,x2,…,xi的所有约束意味着j(j<=i)元组(x1,x2,…,xj)一定也满足D中仅涉及到x1,x2,…,xj的所有约束,i=1,2,…,n。换句话说,只要存在0≤j≤n-1,使得(x1,x2,…,xj)违反D中仅涉及到x1,x2,…,xj的约束之一,则以(x1,x2,…,xj)为前缀的任何n元组(x1,x2,…,xj,xj+1,…,xn)一定也违反D中仅涉及到x1,x2,…,xi的一个约束,n≥i≥j。因此,对于约束集D具有完备性的问题P,一旦检测断定某个j元组(x1,x2,…,xj)违反D中仅涉及x1,x2,…,xj的一个约束,就可以肯定,以(x1,x2,…,xj)为前缀的任何n元组(x1,x2,…,xj,xj+1,…,xn)都不会是问题P的解,因而就不必去搜索它们、检测它们。回溯法正是针对这类问题,利用这类问题的上述性质而提出来的比枚举法效率更高的算法。

使用回溯法的一般步骤:

(1)针对特定问题,确定问题的解空间

(2)确定易于搜索的解空间结构

(3)以深度优先方式搜索解空间,并在搜索过程中用减枝函数避免无效搜索。

下面我们讲述几个使用回溯法的例子。

首先我们先看一个关于象棋的问题,假设我们在一个4*4的棋盘上有一个马,马的位置为(1,1)点,当然也可以是其它点,我们要求列出所有的从(1,1)出发,最终能够回到起点的所有走法。

下面我们给出实现

  1. package com.application.sample;
  2. //使用回溯思想
  3. public class backtrackingSample1 {
  4. static int[][] direction = {
  5. {-1, -1, -2, -2, 2, 2, 1, 1},{-2, 2, 1, -1, 1, -1, 2, -2}};
  6. private int[][] chess;
  7. private int x,y;
  8. private int total=0;
  9. public backtrackingSample1(int x,int y)
  10. {
  11. this.x=x;
  12. this.y=y;
  13. this.chess=new int[4][4];
  14. chess[x][y]=1;
  15. }
  16. private void output()
  17. {
  18. // if(isOneStep())
  19. // {
  20. // System.out.println("Total:"+this.total);
  21. // for(int i=0;i<4;i++)
  22. // {
  23. // for(int j=0;j<4;j++)
  24. // {
  25. // System.out.print(chess[i][j]+" ");
  26. // }
  27. // System.out.println();
  28. // }
  29. // System.out.println();
  30. // }
  31. System.out.println("Total:"+this.total);
  32. for(int i=0;i<4;i++)
  33. {
  34. for(int j=0;j<4;j++)
  35. {
  36. System.out.print(chess[i][j]+" ");
  37. }
  38. System.out.println();
  39. }
  40. System.out.println();
  41. }
  42. public void findway(int posx,int posy,int step)
  43. {
  44. int px,py;
  45. for(int i=0;i<8;i++)
  46. {
  47. px=posx+direction[0][i];
  48. py=posy+direction[1][i];
  49. if(px>=0&&px<=3&&py>=0&&py<=3)
  50. {
  51. if(px==this.x&&py==this.y)
  52. {
  53. this.total++;
  54. this.output();
  55. }else if(chess[px][py]==0)
  56. {
  57. chess[px][py]=step;
  58. findway(px,py,step+1);
  59. chess[px][py]=0;
  60. }
  61. }
  62. }
  63. }
  64. public boolean isOneStep()
  65. {
  66. boolean result=true;
  67. for(int i=0;i<4;i++)
  68. {
  69. for(int j=0;j<4;j++)
  70. {
  71. if(chess[i][j]>2)
  72. {
  73. return false;
  74. }
  75. }
  76. }
  77. return result;
  78. }
  79. public static void main(String[] args) {
  80. // TODO Auto-generated method stub
  81. backtrackingSample1 sample=new backtrackingSample1(1,1);
  82. long start=System.currentTimeMillis();
  83. sample.findway(1, 1, 2);
  84. long end=System.currentTimeMillis();
  85. System.out.println("用时:"+(end-start)+"毫秒");
  86. }
  87. }

运行结果如下所示:

  1. Total:1 0 0 5 2 4 1 0 0 0 0 3 6 0 0 0 0 Total:2 9 12 5 2 4 1 8 11 13 10 3 6 0 7 14 0 ........
  2. Total:369
  3. 0 6 0 0
  4. 4 1 10 7
  5. 11 8 5 2
  6. 0 3 12 9
  7. Total:370
  8. 0 0 0 0
  9. 4 1 0 0
  10. 0 0 5 2
  11. 6 3 0 0
  12. 用时:78毫秒

接下来我们看另外一个经典的问题九宫格。九宫格就是一个9*9的网格,而这个网格又可以被分成9个3*3的子网格,向网格中填入1~9的数字,要求每行,每列以及每个子网格中1-9都只出现一次,即不能有重复数字出现。如下所示,0表示待填入数据的格子

103000509
002109400
000704000
300502006
060000050
700803004
000401000
009205800
804000107

我们将9个子网格的编排如下所示:



















0 1 2
3 4 5
6 7 8

对于用于标识元素在大的9*9网格中位置的变量i和j,有0<=i,j<=8,为了获取(i,j)元素在哪个小子网格中,我们做如下处理
































































i i/3 j j/3
0 0 0 0
1 0 1 0
2 0 2 0
3 1 3 1
4 1 4 1
5 1 5 1
6 2 6 2
7 2 7 2
8 2 8 2

假设k为小的3*3子网格的序号,则我们根据下面的关系可以得到(i,j)与k的关系






















































i/3 j/3 k
0 0 0
0 1 1
0 2 2
1 0 3
1 1 4
1 2 5
2 0 6
2 1 7
2 2 8

我们可以容易得到k=(i/3)*3+(j/3);

下面给出java的实现:

  1. package com.application.sample;
  2. //使用回溯法求解九宫格问题
  3. import java.util.Scanner;
  4. public class BackTrackingSample2 {
  5. private int[][] table;
  6. private boolean[][] rstate;//rstate[i][value]=true表示value在第i行出现过
  7. private boolean[][] cstate;//cstate[j][value]=true表示value在第j行出现过
  8. private boolean[][] gstate;//gstate[k][value]=true表示value在第k个小网格中出现过
  9. private int[] pos;//存放需要填数字的方格位置
  10. private int posnum=0;//需要填的格子总数
  11. private boolean flag=false;//搜索结束标志
  12. public static void main(String[] args) {
  13. // TODO Auto-generated method stub
  14. BackTrackingSample2 sample=new BackTrackingSample2();
  15. sample.searchSolution(1);
  16. }
  17. public BackTrackingSample2()
  18. {
  19. table=new int[9][9];
  20. rstate=new boolean[9][10];
  21. cstate=new boolean[9][10];
  22. gstate=new boolean[9][10];
  23. pos=new int[81];
  24. System.out.println("输入九宫格的数据(使用0表示待填入数据的格子):");
  25. Scanner in=new Scanner(System.in);
  26. String array=null;
  27. for(int i=0;i<9;i++)
  28. {
  29. array=in.nextLine();
  30. for(int j=0;j<9;j++)
  31. {
  32. table[i][j]=array.charAt(j)-'0';
  33. if(table[i][j]!=0)
  34. {
  35. rstate[i][table[i][j]]=true;
  36. cstate[j][table[i][j]]=true;
  37. int k=(i/3)*3+(j/3);
  38. gstate[k][table[i][j]]=true;
  39. }else
  40. {
  41. pos[posnum++]=i*9+j;
  42. }
  43. }
  44. }
  45. pos[posnum]=-1;
  46. }
  47. private void output()//输出结果
  48. {
  49. System.out.println();
  50. System.out.println("结果:");
  51. for(int i=0;i<9;i++)
  52. {
  53. for(int j=0;j<9;j++)
  54. {
  55. System.out.printf("%2d", table[i][j]);
  56. }
  57. System.out.println();
  58. }
  59. }
  60. public void searchSolution(int n)//搜索处理第n个待填入数据的格子
  61. {
  62. if(n>this.posnum)//如果所有的格子都处理完了则输出
  63. {
  64. output();
  65. return;
  66. }
  67. int row=pos[n-1]/9;//获取第n个待处理格子的坐标位置和子网格位置
  68. int column=pos[n-1]%9;
  69. int k=(row/3)*3+(column/3);
  70. for(int i=1;i<=9&&!flag;i++)//遍历,分别用1-9中的数进行试探
  71. {
  72. if(rstate[row][i]) //判断数组i在格所在的行和列以及子网格中是否出现过
  73. continue;
  74. if(cstate[column][i])
  75. continue;
  76. if(gstate[k][i])
  77. continue;
  78. table[row][column]=i;//填入该数,并设置标志
  79. rstate[row][i]=true;
  80. cstate[column][i]=true;
  81. gstate[k][i]=true;
  82. searchSolution(n+1);//回溯
  83. table[row][column]=0;//清除标志
  84. rstate[row][i]=false;
  85. cstate[column][i]=false;
  86. gstate[k][i]=false;
  87. }
  88. }
  89. }

运行结果如下所示:

  1. 输入九宫格的数据(使用0表示待填入数据的格子):
  2. 103000509
  3. 002109400
  4. 000704000
  5. 300502006
  6. 060000050
  7. 700803004
  8. 000401000
  9. 009205800
  10. 804000107
  11. 结果:
  12. 1 4 3 6 2 8 5 7 9
  13. 5 7 2 1 3 9 4 6 8
  14. 9 8 6 7 5 4 2 3 1
  15. 3 9 1 5 4 2 7 8 6
  16. 4 6 8 9 1 7 3 5 2
  17. 7 2 5 8 6 3 9 1 4
  18. 2 3 7 4 8 1 6 9 5
  19. 6 1 9 2 7 5 8 4 3
  20. 8 5 4 3 9 6 1 2 7

我们再讲一个比较经典的问题,那就是八皇后问题。什么是八皇后问题呢?八皇后问题,是一个古老而著名的问题,是回溯算法的典型例题。该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

Java代码实现如下所示:

  1. package com.application.sample;
  2. //使用回溯法解决八皇后问题
  3. public class BackTracingSample4 {
  4. private boolean[][] table=new boolean[8][8];
  5. private int queencount=0;
  6. private int total=0;
  7. public boolean lineHasQueen(int line)//line列全为false则返回true,否则返回false
  8. {
  9. for(int i=0;i<8;i++)
  10. {
  11. if(table[i][line]==true)
  12. return true;
  13. }
  14. return false;
  15. }
  16. public boolean catercornerHasQueen(int x,int y)//判断对角线方向是否含有皇后,如果有则返回true,否则返回false
  17. {
  18. int i,j;
  19. for(i=x,j=y;i<8&&j<8;i++,j++)
  20. {
  21. if(table[i][j]==true)
  22. return true;
  23. }
  24. for(i=x,j=y;i>=0&&j>=0;i--,j--)
  25. {
  26. if(table[i][j]==true)
  27. return true;
  28. }
  29. for(i=x,j=y;i>=0&&j<8;i--,j++)
  30. {
  31. if(table[i][j]==true)
  32. return true;
  33. }
  34. for(i=x,j=y;i<8&&j>=0;i++,j--)
  35. {
  36. if(table[i][j]==true)
  37. return true;
  38. }
  39. return false;
  40. }
  41. public void searchSolution(int line)
  42. {
  43. if(line==8)
  44. return;
  45. for(int i=0;i<8;i++)
  46. {
  47. if((!lineHasQueen(i))&&(!catercornerHasQueen(line,i)))
  48. {
  49. table[line][i]=true;
  50. queencount++;
  51. if(queencount==8)
  52. {
  53. System.out.println();
  54. output();
  55. table[line][i]=false;
  56. queencount--;
  57. continue;
  58. }
  59. searchSolution(line+1);
  60. table[line][i]=false;
  61. queencount--;
  62. }
  63. }
  64. }
  65. public void output()
  66. {
  67. total++;
  68. System.out.println("Total:"+total);
  69. for(int i=0;i<8;i++)
  70. {
  71. for(int j=0;j<8;j++)
  72. {
  73. if(table[i][j]==true)
  74. {
  75. System.out.print("1");
  76. }else
  77. {
  78. System.out.print("0");
  79. }
  80. }
  81. System.out.println();
  82. }
  83. }
  84. public static void main(String[] args) {
  85. // TODO Auto-generated method stub
  86. BackTracingSample4 sample=new BackTracingSample4();
  87. sample.searchSolution(0);
  88. }
  89. }

运行结果如下所示:

Total:1
10000000
00001000
00000001
00000100
00100000
00000010
01000000
00010000

Total:2
10000000
00000100
00000001
00100000
00000010
00010000
01000000

………….

Total:91
00000001
00100000
10000000
00000100
01000000
00001000
00000010
00010000

Total:92
00000001
00010000
10000000
00100000
00000100
01000000
00000010
00001000

最后我们来看一个大家基本都见过的题目,那就是上楼梯。对于一个含有n阶的楼梯,如果我们每次只能上一阶或者两阶,问我们总共可以有多少种走法,每种走法怎么走?

对于这个问题,我们很明显也可以使用回溯法进行求解。求解过程如下所示:

  1. package com.application.sample;
  2. public class BackTracingSample5 {
  3. private int[] step;
  4. public BackTracingSample5(int n)
  5. {
  6. step=new int[n+1];
  7. step[0]=n;
  8. }
  9. public void goUp(int level,int n)
  10. {
  11. if(n>step[0])
  12. return;
  13. if(n==step[0])
  14. {
  15. output(level);
  16. return;
  17. }
  18. for(int i=1;i<=2;i++)
  19. {
  20. step[level+1]=i;
  21. goUp(level+1,n+i);
  22. step[level+1]=0;
  23. }
  24. }
  25. private void output(int level)
  26. {
  27. System.out.println("步数:"+level);
  28. for(int i=1;i<=level;i++)
  29. {
  30. System.out.print(step[i]+" ");
  31. }
  32. System.out.println();
  33. }
  34. public static void main(String[] args) {
  35. // TODO Auto-generated method stub
  36. BackTracingSample5 sample=new BackTracingSample5(5);
  37. sample.goUp(0, 0);
  38. }
  39. }

运行结果如下所示:

步数:5
1 1 1 1 1
步数:4
1 1 1 2
步数:4
1 1 2 1
步数:4
1 2 1 1
步数:3
1 2 2
步数:4
2 1 1 1
步数:3
2 1 2
步数:3
2 2 1

发表评论

表情:
评论列表 (有 0 条评论,227人围观)

还没有评论,来说两句吧...

相关阅读

    相关 回溯总结

    回溯法分为组合,子集,排列等问题,下面分别就这几个问题进行总结. 1.总体架构 总体来说,回溯法像是在遍历一棵树,而这棵树的深度,由回溯的终止条件以及for循环内部的变量控

    相关 回溯

    一、回溯法的基本思想   在问题的解空间树中,按深度优先策略,从根节点出发搜素解空间树。算法搜素至解空间树的任一结点时,先判断该结点是否包含问题的解,如果肯定不包含,则跳

    相关 回溯专题

    回溯法 全排列问题 N皇后问题 枚举,排列,组合问题都可以用回溯法来求解,它也是一个通用的求解问题的算法 全排列问题 比如给你数组1,2,3,

    相关 装载问题-回溯

    有两艘货船,载重分别为w1、w2,物品总重量不超过载重总量w1+w2,问物品是否都可以装下。如,w1=w2=10,物品g1=g2=9,g3=2,则无法装下;w1=w2=5,w3

    相关 回溯算法(试探

    算法思路 基本思想: 为了求得问题的解,先选择某一种可能情况进行试探,在试探过程中,一旦发现原来选择的假设情况是错误的,就退回一步重新选择,继续向另一个方向试

    相关 回溯举例分析

    回溯法,按照百度百科的介绍,是指一种选优的搜索法,按照选优条件向前搜索,以达到目标,但当搜索到某一步时发现原先的选择并不优或者不能达到目标,则退回上一步重新选择,这种走不通就退