回溯法 有时会遇到这样一类题目,它的问题可以分解,但是又不能得出明确的动态规划或是递归解法,此时可以考虑用回溯法解决此类问题。回溯法的优点 在于其程序结构明确,可读性强,易于理解,而且通过对问题的分析可以大大提高运行效率。但是,对于可以得出明显的递推公式迭代求解的问题,还是不要用回溯 法,因为它花费的时间比较长。 回溯法的基本思想 对于用回溯法求解的问题,首先要将问题进行适当的转化,得出状态空间树。 这棵树的每条完整路径都代表了一种解的可能。通过深度优先搜索这棵树,枚举每种可能的解的情况;从而得出结果。但是,回溯法中通过构造约束函数,可以大大 提升程序效率,因为在深度优先搜索的过程中,不断的将每个解(并不一定是完整的,事实上这也就是构造约束函数的意义所在)与约束函数进行对照从而删除一些 不可能的解,这样就不必继续把解的剩余部分列出从而节省部分时间。 回溯法中,首先需要明确下面三个概念: (一)约束函数:约束函数是根据题意定出的。通过描述合法解的一般特征用于去除不合法的解,从而避免继续搜索出这个不合法解的剩余部分。因此,约束函数是对于任何状态空间树上的节点都有效、等价的。 (二)状态空间树:刚刚已经提到,状态空间树是一个对所有解的图形描述。树上的每个子节点的解都只有一个部分与父节点不同。 (三)扩展节点、活结点、死结点:所谓扩展节点,就是当前正在求出它的子节点的节点,在DFS中,只允许有一个扩展节点。活结点就是通过与约束函数的对照,节点本身和其父节点均满足约束函数要求的节点;死结点反之。由此很容易知道死结点是不必求出其子节点的(没有意义)。 深度优先搜索(DFS)和广度优先搜索(FIFO) 在 分支界限法中,一般用的是FIFO或最小耗费搜索;其思想是一次性将一个节点的所有子节点求出并将其放入一个待求子节点的队列。通过遍历这个队列(队列在 遍历过程中不断增长)完成搜索。而DFS的作法则是将每一条合法路径求出后再转而向上求第二条合法路径。而在回溯法中,一般都用DFS。为什么呢?这是因 为可以通过约束函数杀死一些节点从而节省时间,由于DFS是将路径逐一求出的,通过在求路径的过程中杀死节点即可省去求所有子节点所花费的时间。FIFO 理论上也是可以做到这样的,但是通过对比不难发现,DFS在以这种方法解决问题时思路要清晰非常多。 因此,回溯法可以被认为是一个有过剪枝的DFS过程。 利用回溯法解题的具体步骤 首先,要通过读题完成下面三个步骤: (1)描述解的形式,定义一个解空间,它包含问题的所有解。 (2)构造状态空间树。 (3)构造约束函数(用于杀死节点)。 然后就要通过DFS思想完成回溯,完整过程如下: (1)设置初始化的方案(给变量赋初值,读入已知数据等)。 (2)变换方式去试探,若全部试完则转(7)。 (3)判断此法是否成功(通过约束函数),不成功则转(2)。 (4)试探成功则前进一步再试探。 (5)正确方案还未找到则转(2)。 (6)已找到一种方案则记录并打印。 (7)退回一步(回溯),若未退到头则转(2)。 (8)已退到头则结束或打印无解。 回溯法的数据结构 回溯法的状态空间树,在计算机上的数据结构有两种表示方法。当用k表示树的节点层数,n表示节点总数,m表示解的总数时: (一)用m个k元组表示m种不同的解。其中,每组中的元素是[1,n]中的一个元素。在Pascal中,可以这样定义变量: var a:array[1..k,1..m]of integer;(integer可以依n的范围决定) (二)用m个n元组表示m种不同的解。因为所有的节点都包含在每个解的表示中,每组中的元素只有两种情况,被选用和不被选用。在Pascal中,可以这样定义变量: var a:array[1..n,1..m]of boolean; 这两种数据结构的解空间最多都含有2n个不同的元组。 应用举例 (一)N皇后问题 国际象棋的N皇后问题一直是回溯算法的经典问题。该题题目要求如下: 在国际象棋棋盘上放置八个皇后,使她们不能相吃。国际象棋中的皇后可以吃掉与她处于同一行、同一列、同一斜角线(与行和列分别成45度角的所有线)上的棋子。因此每一行只能摆放一个皇后。因共有八行。所以每行有且只有一个皇后。 下面是N=8时候的具体分析(N为其它值时解法类似): 首 先,皇后的位置有一个一维数组来存放A(I)=J表示第I行皇后放在第J列.下面主要来看看怎么样判断皇后是否安全(不会被吃,这就是构造约束函数的过 程)的问题。(1)首先,用一维数组来表示,已经解决了不在同一行的问题。(2)对于列可以引进一个标志数组C[J],若J列上已放了皇后,则C[J]= FALSE。(3)对于左上右下的斜角线I-J为一常量,位于[-7,+7]之间,再此引入标志数组L[-7..7];对于左下右上的对角线,类似的有I +J等于常量,用数组R[2..16]来表示.当在第I行,第J列上放置了皇后,则只需设置:C[J]:=FALSE; L[I-J]:=FLASE; R[I+J]:=FALSE就可以解决皇后的安全问题了. 源程序如下: Program EightQueens; var cont,i:integer; a:array[1..8] of byte; c:array[1..8] of boolean; l:array[-7..7] of boolean; r:array[2..16] of boolean; q:boolean; procedure pr; var i:integer; begin for i:=1 to 8 do write(a[i]:4); inc(cont);writeln('cont=',cont); end; procedure try(i:integer); var j:integer; procedure erase(i:integer); begin c[j]:=true;l[i-j]:=true;r[i+j]:=true; end; begin for j:=1 to 8 do begin if c[j] and l[i-j] and r[i+j] then begin a[i]:=j;c[j]:=false;l[i-j]:=false; r[i+j]:=false; if i<8 then try(i+1) else pr; erase(i); end; end; end; begin for i:=1 to 8 do c[i]:=true; for i:=-7 to 7 do l[i]:=true; for i:=2 to 16 do r[i]:true; cont:=0; i:=1; try(i); readln; end. (二)圆盘问题 题 目要求:从左向右依次安放 4 根细柱 A,B,C,D. 在 A 上套有 N (N≤20) 个直径相同的圆盘, 从下到上依次用连续的小写字母 a,b,c,...编号, 将这些圆盘经过B, C 单向地移入 D (即不允许从右向左移动). 圆盘可在 B,C 中暂存. 从键盘输入 N, 及前 N 个小写字母的一个排列, 它表示最后在 D 盘上形成的一个从下到上的圆盘序列。请用文本文件 ANS2.TXT 输出形成这一排列的操作过程。该文件的每一行为一个形如 "k M L" 的字母序列, 其中 k 为圆盘编号, M 为k 盘原先的柱号, L 为新柱号。或者直接在屏幕上输出"No", 表示不能生成这种排列。 例: ┃ ┃ ┃ ┃ 键盘输入: ┃ ┃ ┃ ┃ 3 d ━╋━ ┃ ┃ ┃ acb c ━╋━ ┃ ┃ ┃ 则一个正确的输出文件 b ━╋━ ┃ ┃ ┃ 可以是: a ━╋━ ┃ ┃ ┃ c A B ━━┻━━━┻━━━┻━━━┻━ b A C A B C D a A D b C D c B D 本 题是递归回溯方法的一个典型的应用.以D柱的圆盘序列为检验的标准,一旦达到目标便结束,否则考虑下一种移动方案.将D柱上的圆盘序列从下到上依次编号为 1,2,...,N,利用字母a,b,c,...与数字1,2,...,N的对应关系,在A柱上得到了数字1,2,...,N的一个排列.这一变换在很大 程度上简化了算法。 比如,A柱,D柱的字母序列分别为(a,b,c,d,e)和(e,c,a,b,d),则变换后,A柱,D柱的数字序列分别为(3,4,2,5,1)和(1,2,3,4,5)。 容易看出,在D柱上,盘号从下到上必须是连续递增的。在C柱上,盘号从下到上必须是递减的(不要求连续,因为C柱只能移到D柱)。在A和B柱上,盘号的顺序是随意的。 由此可归纳出以下的算法: 1.记四根柱上顶部(即栈顶)的圆盘编号依次为topa,topb,topc,topd, 则能够从 A,B,C 三根柱子直接将顶部圆盘移入D柱的充分必要条件是:topa=succ(topd)或topb=succ(topd)或topc=succ(topd)。 2.此外还有三种移动方式:A=>B,A=>C,B=>C,能进行这三种移动的条件依次是(1)topa>0 (2)topa>0且topa<topc (3)topb>0且topb<topc。这三种移动方式要递归地进行,且每一步 都要考虑三种移动方式,在作出认真的论证之前,不要轻易地放弃哪一种,以免造成解的丢失。(DFS过程) 3.当A,B,C三根柱子移空时程序正常结束。当递归回溯全部完成而没有得到目标状态时,以“无解”结束。 结语 回溯法适用于在计算机奥林匹克竞赛中解决难以归纳一般规律解法的问题,其适用范围广,灵活性大,在解一些列举方法的问题时尤其可用。但是,其缺点也是明显的,即时间复杂度较大;因此在采用时还需要谨慎。 |
算法——回溯法详细运用
最新推荐文章于 2022-08-02 00:32:06 发布