引言
寻找问题的解的一种可靠的方法是首先列出所有候选解,然后依次检查每一个,在检查完所有或部分候选解后,即可找到所需要的解。理论上,当候选解数量有限并且通过检查所有或部分候选解能够得到所需解时,上述方法是可行的。不过,在实际应用中,很少使用这种方法,因为候选解的数量通常都非常大(比如指数级,甚至是大数阶乘),即便采用最快的计算机也只能解决规模很小的问题。对候选解进行系统检查的方法有多种,其中回溯和分枝定界法是比较常用的两种方法。按照这两种方法对候选解进行系统检查通常会使问题的求解时间大大减少(无论对于最坏情形还是对于一般情形)。事实上,这些方法可以使我们避免对很大的候选解集合进行检查,同时能够保证算法运行结束时可以找到所需要的解。因此,这些方法通常能够用来求解规模很大的问题。
算法思想
回溯(backtracking)是一种系统地搜索问题解答的方法。为了实现回溯,首先需要为问题定义一个解空间(solution space),这个空间必须至少包含问题的一个解(可能是最优的)。
下一步是组织解空间以便它能被容易地搜索。典型的组织方法是图(迷宫问题)或树(N皇后问题)。
一旦定义了解空间的组织方法,这个空间即可按深度优先的方法从开始节点进行搜索。
回溯方法的步骤如下:
1) 定义一个解空间,它包含问题的解。
2) 用适于搜索的方式组织该空间。
3) 用深度优先法搜索该空间,利用限界函数避免移动到不可能产生解的子空间。
回溯算法的一个有趣的特性是在搜索执行的同时产生解空间。在搜索期间的任何时刻,仅保留从开始节点到当前节点的路径。因此,回溯算法的空间需求为O(从开始节点起最长路径的长度)。这个特性非常重要,因为解空间的大小通常是最长路径长度的指数或阶乘。所以如果要存储全部解空间的话,再多的空间也不够用。
算法应用
回溯算法的求解过程实质上是一个先序遍历一棵"状态树"的过程,只是这棵树不是遍历前预先建立的,而是隐含在遍历过程中<<数据结构>>(严蔚敏).
(1) 幂集问题(组合问题) (参见《数据结构》(严蔚敏))
求含N个元素的集合的幂集。
如对于集合A={1,2,3},则A的幂集为
p(A)={{1,2,3},{1,2},{1,3},{1},{2,3},{2},{3},Φ}
幂集的每个元素是一个集合,它或是空集,或含集合A中的一个元素,或含A中的两个元素,或者等于集合A。反之,集合A中的每一个元素,它只有两种状态:属于幂集的元素集,或不属于幂集元素集。则求幂集P(A)的元素的过程可看成是依次对集合A中元素进行“取”或“舍”的过程,并且可以用一棵状态树来表示。求幂集元素的过程即为先序遍历这棵状态树的过程。
下面给出程序:
1 #include <vector> 2 #include <iostream> 3 4 using namespace std; 5 6 template<class T> 7 void VecPrint(vector<T> &vec) 8 { 9 for (vector<T>::const_iterator it = vec.begin(); it != vec.end(); it++) 10 cout << *it << " "; 11 12 cout << "#" << endl; 13 } 14 15 template<class T> 16 void PowerSet(int i, vector<T> &vec1, vector<T> &vec2) 17 { 18 int k = 0; 19 T data = 0; 20 if (i >= vec1.size()) 21 { 22 VecPrint(vec2); 23 } 24 else 25 { 26 // 取第i个数,求幂集 27 vec2.push_back(vec1[i]); 28 PowerSet(i+1, vec1, vec2); 29 30 // 舍第i个数,求幂集 31 vec2.pop_back(); 32 PowerSet(i+1, vec1, vec2); 33 } 34 35 } 36 37 int _tmain() 38 { 39 vector<int> vec1; 40 vector<int> vec2; 41 42 vec1.push_back(1); 43 vec1.push_back(2); 44 vec1.push_back(3); 45 46 // 求{1,2,3的幂集} 47 PowerSet(0, vec1, vec2); 48 return 0; 49 50 }
结果:
(2)迷宫问题(参见《数据结构》(严蔚敏))
计算机解迷宫时,通常用的是"试探和回溯"的方法,即从入口出发,顺某一方向向前探索,若能走通,则继续往前走;否则沿原路退回,换一个方向再继续探索,直至所有可能的通路都探索到为止,如果所有可能的通路都试探过,还是不能走到终点,那就说明该迷宫不存在从起点到终点的通道。
1.从入口进入迷宫之后,不管在迷宫的哪一个位置上,都是先往东走,如果走得通就继续往东走,如果在某个位置上往东走不通的话,就依次试探往南、往西和往北方向,从一个走得通的方向继续往前直到出口为止;
2.如果在某个位置上四个方向都走不通的话,就退回到前一个位置,换一个方向再试,如果这个位置已经没有方向可试了就再退一步,如果所有已经走过的位置的四个方向都试探过了,一直退到起始点都没有走通,那就说明这个迷宫根本不通;
3.所谓"走不通"不单是指遇到"墙挡路",还有"已经走过的路不能重复走第二次",它包括"曾经走过而没有走通的路"。显然为了保证在任何位置上都能沿原路退回,需要用一个"后进先出"的结构即栈来保存从入口到当前位置的路径。并且在走出出口之后,栈中保存的正是一条从入口到出口的路径。
由此,求迷宫中一条路径的算法的基本思想是:
若当前位置"可通",则纳入"当前路径",并继续朝"下一位置"探索;若当前位置"不可通",则应顺着"来的方向"退回到"前一通道块",然后朝着除"来向"之外的其他方向继续探索;若该通道块的四周四个方块均"不可通",则应从"当前路径"上删除该通道块。
设定当前位置的初值为入口位置;
do{
若当前位置可通,
则{
将当前位置插入栈顶; // 纳入路径
若该位置是出口位置,则算法结束;
// 此时栈中存放的是一条从入口位置到出口位置的路径
否则切换当前位置的东邻方块为新的当前位置;
}
否则
{
若栈不空且栈顶位置尚有其他方向未被探索,
则设定新的当前位置为: 沿顺时针方向旋转找到的栈顶位置的下一相邻块;
若栈不空但栈顶位置的四周均不可通,
则{ 删去栈顶位置; // 从路径中删去该通道块
若栈不空,则重新测试新的栈顶位置,
直至找到一个可通的相邻块或出栈至栈空;
}
}
} while (栈不空);
程序如下:
1 #include "stdafx.h" 2 3 #include <stdio.h> 4 #include <stack> 5 6 using namespace std; 7 8 9 enum enPosType {WALL, CORRIDOR}; // 位置类型:墙,通道 10 enum enGridSize {ROW_NUM = 7, COL_NUM = 13}; // 迷宫尺寸 11 enum enDirection {EAST, SOUTH, WEST, NORTH}; // 移动方向 12 enum enCrossType {TRIED = 2, PATH = 9}; 13 14 struct Position 15 { 16 int row; 17 int col; 18 Position(int a=0, int b=0):row(a),col(b){}; 19 }; 20 21 struct PathMsg 22 { 23 Position pos; 24 enDirection dir; 25 }; 26 27 // 迷宫 28 // 在迷宫外层加一圈墙 29 int grid[ROW_NUM+2][COL_NUM+2] = 30 { 31 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 32 {0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0}, 33 {0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0}, 34 {0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0}, 35 {0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0}, 36 {0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0}, 37 {0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 38 {0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, 39 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} 40 }; 41 42 // ====================================== 43 //当前位置是否可以通过 44 // ====================================== 45 bool Valid(Position pos) 46 { 47 if( pos.row>=1 && pos.row<(ROW_NUM+1) && 48 pos.col>=1 && pos.col<(COL_NUM+1) && 49 grid[pos.row][pos.col]==CORRIDOR 50 ) 51 { 52 return true; 53 } 54 else 55 { 56 return false; 57 } 58 } 59 60 // ====================================== 61 // 当前位置的下一个位置 62 // ====================================== 63 Position NextPos(Position cur,enDirection dir) 64 { 65 Position next; 66 67 switch(dir) 68 { 69 case EAST: 70 next.row = cur.row; 71 next.col = cur.col + 1; 72 break; 73 case SOUTH: 74 next.row = cur.row + 1; 75 next.col = cur.col; 76 break; 77 case WEST: 78 next.row = cur.row; 79 next.col = cur.col - 1; 80 break; 81 case NORTH: 82 next.row = cur.row - 1; 83 next.col = cur.col; 84 break; 85 } 86 87 return next; 88 } 89 //是否到达终点 90 bool Done(Position cur,Position end) 91 { 92 if(cur.row==end.row && cur.col==end.col) 93 return true; 94 else 95 return false; 96 } 97 98 // ====================================== 99 // 100 // ====================================== 101 void SetCrossType(Position pos, enCrossType entype) 102 { 103 grid[pos.row][pos.col] = entype; 104 } 105 106 // ====================================== 107 // 寻找迷宫路径 108 // ====================================== 109 bool MazePath(stack<PathMsg> &path,Position start,Position end) 110 { 111 Position curpos = start; 112 113 enDirection dir = EAST; 114 115 PathMsg pathmsg; 116 117 do 118 { 119 if(Valid(curpos)) 120 { 121 SetCrossType(curpos, PATH); 122 pathmsg.pos = curpos; 123 pathmsg.dir = EAST; 124 path.push(pathmsg); 125 126 if(Done(curpos,end)) 127 return true; 128 129 curpos = NextPos(curpos,EAST); 130 dir = EAST; 131 } 132 else 133 { 134 if (!path.empty()) 135 { 136 pathmsg = path.top(); 137 path.pop(); 138 139 while (dir == NORTH && (!path.empty())) 140 { 141 SetCrossType(pathmsg.pos, TRIED); 142 pathmsg = path.top(); 143 path.pop(); 144 } 145 146 if (pathmsg.dir < NORTH) 147 { 148 dir = (enDirection)(pathmsg.dir+1); 149 pathmsg.dir = dir; 150 path.push(pathmsg); 151 152 curpos = NextPos(pathmsg.pos, dir); 153 } 154 } 155 } 156 }while(!path.empty()); 157 158 return false; 159 } 160 161 162 int main() 163 { 164 stack<PathMsg> stk; 165 166 Position start(1,1), end(4,1); 167 168 if( MazePath(stk,start,end) ) 169 { 170 //PrintPath(stk); 171 } 172 else 173 printf("not reachable!\n"); 174 175 }
(3)N皇后问题:
在一个N*N的棋盘上放置N个皇后,且使得每两个之间不能互相攻击,也就是使得每两个不在同一行,同一列和同一斜角线上。
对于N=1,问题的解很简单,而且我们很容易看出对于N=2和N=3来说,这个问题是无解的。所让我们考虑4皇后问题并用回溯法对它求解。因为每个皇后都必须分别占据—行,我们需要做的不过是为图1棋盘上的每个皇后分配一列。
我们从空棋盘开始,然后把皇后1放到它所在行的第一个可能位置上,也就是第一行第—列。对于皇后2,在经过第一列和第二列的失败尝试之后,我们把它放在第一个可能的位置,就是格子〔2,3),位于第二行第二列的格子。这被证明是一个死胡同,因为皇后:将没有位置可放。所以,该算法进行回溯,把皇后2放在下一个可能位置(2,4)上。然后皇后3就可以放在(3,2),这被证明是另一个死胡同。该算法然后就回溯到底,把皇后1移到(1,2)。接着皇后2到(2,4),皇后3到(3,1),而皇后4到(4,3),这就是该问题的一个解。图2给出了这个查找的状态空间树。
程序如下:
1 #include <stdio.h> 2 #include <math.h> 3 4 5 #define N 4 6 int col[N+1]; 7 8 // ==================== 9 // 输出结果 10 // ==================== 11 void Output() 12 { 13 14 for(int i=1;i<=N;i++) 15 { 16 printf("(%d,%d)\n",i,col[i]); 17 } 18 19 printf("\n"); 20 } 21 22 // ==================== 23 // 求解函数 24 // ==================== 25 void Queen(int start,int end) 26 { 27 if(start > end) 28 Output(); 29 else 30 { 31 for(int i = 1;i <= end; i++) 32 { 33 col[start] = i; // 皇后放第i个位置 34 35 int k; 36 for (k = 1; k < start; k++) 37 { 38 if (col[k] != col[start] && (abs(col[k] - col[start]) != abs(k-start))) 39 { 40 } 41 else 42 break; 43 } 44 45 if ( k == start) 46 Queen(start+1, end); 47 } 48 } 49 } 50 51 int _tmain() 52 { 53 printf("the answer is:\n"); 54 for(int i=1;i<=N;i++) 55 { 56 col[1]=i; //设置第一行 57 Queen(2,N); 58 } 59 return 0; 60 61 }