定义
回溯算法(Backtracking)在很多场景中会使用,如N皇后,数迷,集合等,其是暴力求解的一种优化。参考https://en.wikipedia.org/wiki/Backtracking 中的说明,定义如下:
Backtracking is a general algorithm for finding all (or some) solutions to some computational problems, notably constraint satisfaction problems, that incrementally builds candidates to the solutions, and abandons each partial candidate c (“backtracks”) as soon as it determines that c cannot possibly be completed to a valid solution
从上文中可以得出, 核心的含义是 回溯算法是通过一步一步(通常是用递归)构建可能”解”,并且回溯不可能”解”来求所有或者部分解决方案的通用算法。其中“回溯”的具体意思就是将不可能解或者部分解的候选尽早的舍弃掉,“解”需要满足一定的限制条件(constraint satisfaction)
通用算法
这里介绍一下回溯算法的通用思想,一般来讲,会设置一个递归函数,函数的参数会携带一些当前的可能解的信息,根据这些参数得出可能解或者不可能而回溯。
参考 http://web.cse.ohio-state.edu/~gurari/course/cis680/cis680Ch19.html 这里面有一个通用的算法
ALGORITHM try(v1,...,vi) // 这里的V1.....V2携带的参数说明 “可能解”
// 入口处验证是否是全局解,如果是,直接返回。
// 实际编程中也需要查看是否是无效解,如果是,也是直接返回
IF (v1,...,vi) is a solution THEN RETURN (v1,...,vi)
FOR each v DO // 对于每一个可能的解,进行查看
// 下面的含义是形成一个可能解 进行递归
IF (v1,...,vi,v) is acceptable vector THEN
sol = try(v1,...,vi,v)
IF sol != () THEN RETURN sol
// 这个地方其实需要增加“回溯” 处理,实际编程中通常是函数参数的变化
END
END
RETURN ()
经典算法
- N皇后问题
这个是一个比较经典的问题, 意思是将N个“皇后”放在N*N的棋盘上,每个皇后不能再同一列,同一行,和斜对角 (这个就是限制条件constraint satisfaction)。 下面是回溯算法的需求所有的可能解。
算法基本的步骤思想为:
1)从第一行开始
2 )如果所有的皇后已经放置完成, 生成解,并且返回true
3)尝试当前行的所有列,如果当前行与列是合法的
3.1 修改棋盘让其成为部分解,
3.2 然后递归查看(主要是2, 3,4)该解是否合法
3.3 Backtrack 棋盘进行回溯
4) 如果上述所有的组合都为非法,返回false
// 寻找N皇后问题的可能解, 我们用 '.' 表示不放置皇后,用'Q'表示放置皇后
// 利用vector<string> 表示一个解决方案, vector<vector<string>> 表示
// 所有的解决方案。
// 在递归初,我们可以生成一个待解的棋盘
vector<vector<string>> solveNQueens(int n) {
string tmp (n, '.');
//生成一个N*N待解的棋盘,没有任何皇后
vector<string> broad (n, tmp);
nQueue = n;
vector<vector<string>> ans;
solveNQueensHelper (ans, broad, 0, nQueue );
return ans;
}
// 回溯算法的递归函数,
bool solveNQueensHelper (vector<vector<string>>& ans, vector<string> &broad, int row, int nQueue)
{
// 如果当前的行数大于或者等于皇后数,说明当前棋盘是一个解
// 直接返回
if(row >= nQueue){
ans.push_back(broad);
return true;
}
// 从当前行的列中选取一个可能解
for (int column = 0; column < nQueue; column++){
// 查看一下,当前可能解是否有效,只有有效,才可能继续递归
if(isOk (broad, row, column))
{
// 有效,修改可能解的棋盘
broad[row][column] = 'Q';
// 递归调用是否可能解
if (solveNQueensHelper (ans, broad, row + 1, nQueue)){
// return true;
}
// 回溯, 去生成其他解
broad[row][column] = '.';
}
}
return false;
}
// 查看当前部分解是否有效
bool isOk (const vector<string> &broad, int row, int column){
// 查看
for (int i = 0; i <= row; i++)
{
if(broad [i] [column] == 'Q'){
return false;
}
}
int tmpRow = row;
int tmpColumn = column;
while (tmpRow >= 0 && tmpColumn >= 0)
{
if(broad [tmpRow] [tmpColumn] == 'Q'){
return false;
}
tmpRow--,
tmpColumn--;
}
tmpRow = row;
tmpColumn = column;
while (tmpRow >= 0 && tmpColumn < nQueue)
{
if(broad [tmpRow] [tmpColumn] == 'Q'){
return false;
}
tmpRow--,
tmpColumn++;
}
return true;
}
};
Sudoku (数独)问题
其要求,1-9的数不能出现在同一列,同一行,或者同一个3*3的格子中。
算法思想如下:
1 )找到一个空格子,如果找不到, 说明已经填满,这是一个解,返回true
2 )从1 – 9 的数字中填上找到的空格子,如果合法
2.1 生成部分解,递归调用 1)2) 3)
2.2 如果递归调用返回true,说明合法
2.3 回溯部分解
3)上述都不成,直接返回false
c++的代码实现如下:
// board 是一个9*9的格子,如果空格子,用'.' 表示,否则为1-9的数字
void solveSudoku(vector<vector<char>>& board) {
// 递归调用该函数
helperSolveSudoku (board);
}
bool helperSolveSudoku(vector<vector<char>>& board) {
int row = 0;
int colum = 0;
// step1, 获取一个空格, 如果没找到,说明已经填满。
if ( !GetNextUnsignedOne (board, row, colum) )
return true;
// Step2: 从1-9 分别填上棋盘
for (char num = '1'; num <= '9'; num++ ){
// 查看当前的数字放入棋盘中是否合法
if (isNumOk (board, row, colum, num) ){
// step2.1 生成部分解,递归调用
board[row][colum] = num;
if (helperSolveSudoku (board) )
{
// 如果解合法,直接返回
return true;
}
// 回溯部分解
board[row][colum] = '.';
}
}
// step3 没有解,直接返回false
return false;
}
// 帮助函数以获取一个可能的空位置
bool GetNextUnsignedOne(const vector<vector<char>>& board, int &row, int& column){
for(int i = 0; i < 9; i++){
for(int j = 0; j < 9; j++){
if (board [i] [j] == '.'){
row = i;
column = j;
return true;
}
}
}
return false;
}
// 帮助函数以验证数字在当前是否合法
bool isNumOk (const vector<vector<char>>& board, int row, int column, char num){
// check the row is ok or not
for(int i = 0; i < 9; i++){
if(board [row] [i] == num){
return false;
}
}
// check the colum is ok or not
for(int i = 0; i < 9; i++){
if(board [i] [column] == num){
return false;
}
}
int startRow = row - row % 3;
int startcolumn = column - column %3;
// check the 3*3 inbox is ok for not.
for (int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++)
{
if(board [startRow + i] [startcolumn + j] == num){
return false;
}
}
}
return true;
}
- 集合问题
有一个数组,产生一个集合,该集合能包含所有的元素,如 【1,2,3】数组可以满足的集合就是【1,2,3】【1,3,2】【2,1,3】【2,3,1】【3,1,2】【3,2,1】.
这个也可以用回溯的方式实现
基本算法思想为:
1) 查看当前的集合,是否满足所有元素的集合,如果满足,返回
2) 从数组挑选一个元素,查看当前的元素是否属于部分解
2.1 如果不属于部分解,将元素加入到部分解
2.2 递归调用 1, 2, 3
2.3 回溯该元素
3 数组元素完成,直接返回
具体实现的C++代码如下:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> ans;
vector<int> tmpRes;
permuteHelper (ans, tmpRes, nums);
return ans;
}
// 帮助函数以实现数据集合功能
// ans 表示几个数组
// tmpRes 表示部分解
// nums 表示的当前数组元素个数
void permuteHelper(vector<vector<int>>& ans, vector<int>&tmpRes, vector<int>& nums){
// step1 如果部分解的元素个数和数字的元素相同,直接插入
if(tmpRes.size () == nums.size ()){
ans.push_back (tmpRes);
return;
}
//step2 从集合中选定一个元素
for(int i = 0; i < nums.size(); i++)
{
if(std::find (tmpRes.begin(), tmpRes.end(), nums [i]) == tmpRes.end()){
//step 2.1 如果部分解不包含该元素,
// 其实也可以用一个set<int> 来判定是否含有该元素。
tmpRes.push_back (nums [i]);
//step 2.2 递归调用
permuteHelper (ans, tmpRes, nums);
// 回溯部分解
tmpRes.pop_back();
}
else{
continue;
}
}
// Step3,完成,直接返回
return;
}