基于回溯框架求解之八皇后
上述的题目都是在一维的角度求解回溯问题,比如像一个数组,一个字符串这样去展开递归树,但是有一些题目是二维的,需要我们进行二维的递归树展开。比如像Leetcode面试题 08.12. 八皇后问题, 说的是:设计一种算法,打印N
皇后在N × N
棋盘上的各种摆法,其中每个皇后都不同行、不同列,也不在对角线上。这里的“对角线”指的是所有的对角线,不只是平分整个棋盘的那两条对角线。示例:
输入:4 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] 解释: 4 皇后问题存在如下两个不同的解法。 [ [".Q..", // 解法 1 "...Q", "Q...", "..Q."], ["..Q.", // 解法 2 "Q...", "...Q", ".Q.."] ]
每一行每一列都只能放一个,按照习惯,我们假设一行放一个,这样的话我们就需要对列进行遍历,然后每行依次递增,此时的递归树的全展开代码如下所示:
void backtrack(vector<string>& s, int index, int n){ // index表示行 for(int column=0; column<n; ++column){ s[index][column] ='Q'; backtrack(s, index+1, n); s[index][column] = '.'; } }
按照上述这个全展开,每一行的每个位置都会被放置一个'Q'
的全部结果都会遍历得到,这棵树我们是展开了,接下来就是剪枝了。但是在剪枝之前,我们可以先写一首终止条件,也就是将计算得到的某个结果添加到最终的结果中去。终止条件就是当行遍历到最后一行的时候:
void backtrack(vector<string>& s, int index, int n){ if(index == n){ res.push_back(s); return; } for(int column=0; column<n; ++column){ s[index][column] ='Q'; backtrack(s, index+1, n); s[index][column] = '.'; } }
接下来我们就只需要处理剪枝即可,依据题意,每一行每一列都只能有一个Q
,因为我们遍历的时候就是按照行来遍历的,那么对于每一列,我们要保证它不能与其它列重合,那这不就可以用一个集合来记录哪一列被选中了,此时如果当前列在集合中的话,就剪枝去掉即可。这样列的剪枝就可以实现了:
unordered_set<int> set_column; void backtrack(vector<string>& s, int index, int n){ if(index == n){ res.push_back(s); return; } for(int column=0; column<n; ++column){ if(set_column.count(column)){ // 第一重剪枝 continue; } set_column.insert(column); s[index][column] ='Q'; backtrack(s, index+1, n); s[index][column] = '.'; set_column.erase(column); } }
还有一个就是斜对角的,因为我们是从上往下依次每行放Q
的,所以我们只需要检查左上角,还有右上角上是不是有Q
,有的话,我们也需要去剪枝,我们写一个函数,输入当前的棋盘s
和当前的row
和column
,还有判断棋盘边界必须的棋盘长度n
:
bool Have_Q(vector<string>& s, int row, int column, int n){ for(int i=row-1, j=column-1; i>=0 && j>=0; --i, --j){ if(s[i][j] == 'Q') return true; } for(int i=row-1, j=column+1; i>=0 && j<n; --i, ++j){ if(s[i][j] == 'Q') return true; } return false; }
之后我们就可以设置第二层剪枝了:
void backtrack(vector<string>& s, int index, int n){ if(index == n){ res.push_back(s); return; } for(int column=0; column<n; ++column){ if(set_column.count(column)){ // 第一层剪枝 continue; } if(Have_Q(s, index, column, n)){ // 第二层剪枝 continue; } set_column.insert(column); s[index][column] ='Q'; backtrack(s, index+1, n); s[index][column] = '.'; set_column.erase(column); } }
到此,回溯算法的主逻辑就写完了,全部代码如下所示:
class Solution { public: vector<vector<string>> res; unordered_set<int> set_column; vector<vector<string>> solveNQueens(int n) { vector<string> s(n, string(n, '.')); backtrack(s, 0, n); return res; } void backtrack(vector<string>& s, int index, int n){ // index表示行 if(index == n){ res.push_back(s); return; } for(int column=0; column<n; ++column){ if(set_column.count(column)){ // 第一层剪枝 continue; } if(Have_Q(s, index, column, n)){ // 第二层剪枝 continue; } set_column.insert(column); s[index][column] ='Q'; backtrack(s, index+1, n); s[index][column] = '.'; set_column.erase(column); } } bool Have_Q(vector<string>& s, int row, int column, int n){ for(int i=row-1, j=column-1; i>=0 && j>=0; --i, --j){ if(s[i][j] == 'Q') return true; } for(int i=row-1, j=column+1; i>=0 && j<n; --i, ++j){ if(s[i][j] == 'Q') return true; } return false; } };
基于回溯框架求解之单词搜索
花,花里胡哨的回溯框架的题目它,它又来了。LeetCode 79. 单词搜索:给定一个m x n
二维字符网格board
和一个字符串单词word
。如果word
存在于网格中,返回true
;否则,返回false
。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。示例1
:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED" 输出:true
还是那句话,只要全展开递归树能够全展开,这类题目其实就已经做了一大半了。说白了其实就是在网格里面搜一条路径,那么对于每个网格的位置上面,能够选择的方向只有四个,因此我们可以对这四个方向进行for
循环遍历,然后把这颗递归树展开:
vector<pair<int, int>> direction{{0,1}, {0,-1}, {1, 0}, {-1, 0}}; void backtrack(vector<vector<char>>& board, vector<vector<int>>& vis, string& word, int row, int column, int index){ for(pair<int, int>dir : direction){ row = row + dir.first; column = column + dir.second; backtrack(board, vis, word, row, column, index+1); row = row - dir.first; column = column - dir.second; } }
上述代码中,我们把方向放在一个vector
数组里面,里面存的是一个pair<int, int>
数组。之后还有一个终止条件,还有剪枝需要去处理,我们先来处理一下终止条件,就是当index
访问到了最后一个word
的长度嘛,那就是对word
中的每个字符都检查了一遍。
void backtrack(vector<vector<char>>& board, vector<vector<int>>& vis, string& word, int row, int column, int index){ if(index == word.size()){ // 终止条件满足,返回结果 res = true; return; } for(pair<int, int>dir : direction){ row = row + dir.first; column = column + dir.second; backtrack(board, vis, word, row, column, index+1); row = row - dir.first; column = column - dir.second; } }
剩下的一件事情就是剪枝了,首先就是访问的边界条件,如果越界,我们是需要剪枝的,如果访问到了之前已经访问过的元素,我们也是需要去剪枝的,字符不相等的时候我们也是需要去剪枝的。所以我们需要一个二维数组去记录哪些元素已经被访问过了。然后还需要去写一个剪枝的函数:
void backtrack(vector<vector<char>>& board, vector<vector<int>>& vis, string& word, int row, int column, int index){ if(index == word.size()){ // 终止条件满足,返回结果 res = true; return; } for(pair<int, int>dir : direction){ if(Purning(board, vis, word, row, column, index)) continue; // 剪枝 vis[row][column] = 1; row = row + dir.first; column = column + dir.second; backtrack(board, vis, word, row, column, index+1); row = row - dir.first; column = column - dir.second; vis[row][column] = 0; } } bool Purning(vector<vector<char>>& board, vector<vector<int>>& vis, string& word, int row, int column, int index){ if(row < 0 || row >= board.size() || column < 0 || column >= board[0].size()) return true; // 边界情况剪枝。 if(board[row][column] == word[index] && !vis[row][column]) return false; // 值不相等情况,和已访问过该节点情况,剪枝。 else return true; }
到此整颗递归搜索树就构建完毕了。但是这道题目还有一个骚操作,就是这个递归树展开的根节点是会变动的,也就是说矩阵中的元素与word
单词第一个字符相等的时候我们才需要去递归展开这颗递归树,因此我们可以得到完整代码如下所示:
class Solution { public: bool res = false; vector<pair<int, int>> direction{{0,1}, {0,-1}, {1, 0}, {-1, 0}}; bool exist(vector<vector<char>>& board, string word) { vector<vector<int>> vis(board.size(), vector<int>(board[0].size())); for(int i=0; i<board.size(); ++i){ for(int j=0; j<board[0].size(); ++j){ if(board[i][j] == word[0]){ backtrack(board, vis, word, i, j, 0); if(res) return res; } } } return res; } void backtrack(vector<vector<char>>& board, vector<vector<int>>& vis, string& word, int row, int column, int index){ if(index == word.size()){ // 终止条件满足,返回结果 res = true; return; } for(pair<int, int>dir : direction){ if(Purning(board, vis, word, row, column, index)) continue; // 剪枝 vis[row][column] = 1; row = row + dir.first; column = column + dir.second; backtrack(board, vis, word, row, column, index+1); row = row - dir.first; column = column - dir.second; vis[row][column] = 0; } } bool Purning(vector<vector<char>>& board, vector<vector<int>>& vis, string& word, int row, int column, int index){ if(row < 0 || row >= board.size() || column < 0 || column >= board[0].size()) return true; // 边界情况剪枝。 if(board[row][column] == word[index] && !vis[row][column]) return false; // 值不相等情况,和已访问过该节点情况,剪枝。 else return true; } };
基于回溯框架求解之解数独
等等,还没完,回溯算法还有更骚的题,但是万变不离其宗,我们来看一下LeetCode 37. 解数独,编写一个程序,通过填充空格来解决数独问题。数独的解法需 遵循如下规则:数字1-9
在每一行只能出现一次。数字1-9
在每一列只能出现一次。数字1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)数独部分空格内已填入了数字,空白格用 ‘.’ 表示。示例:
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]] 输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
这道题目与之前的几道不一样的地方在于它还有一些骚操作,如果想要全展开,不仅要对这个二维矩阵进行遍历,并且在每个小格子上面还需要对数字1-9
进行遍历,并且与之前的回溯算法不一样的地方在于,如果这里找到了一个可行解的话,我们就需要返回了,不然会被回溯回去,就相当于什么都没做了。因此我们将回溯函数backtrack()
的返回值类型不再设置为void
类型,而是将其设置为bool
类型,此时全展开的代码可以写成如下形式:
bool backtrack(vector<vector<char>>& board, int index){ if(index == 9*9) return true; int row = index / 9; int column = index % 9; for(int i=1; i<=9; ++i){ // 每个方格上遍历放九个数字 board[row][column] = i+'0'; if(backtrack(board, index+1)) return true; board[row][column] = '.'; } return false; }
上述代码中已经加入了终止条件,我们只需要再加入剪枝即可,这里的剪枝分为两种情况,如果被选中的小方格中有元素的话,我们就跳过,返回下一个元素的求解结果:
if(board[row][column] != '.'){ // 给定行和列上已经有数字后,进行剪枝 return backtrack(board, index+1); }
如果是给定的1-9
中某个数字不合适的话,我们就continue
换掉这个数字,可以写一个剪枝函数来实现这个事情:
bool Pruning(vector<vector<char>>& board, int row, int column, char str_i){ for(int i=0; i<9; ++i){ if(board[row][i] == str_i) return true; // 对列遍历,剪枝 if(board[i][column] == str_i) return true; // 对行遍历,剪枝 } int row_new = (row/3)*3, column_new = (column/3)*3; // 对小的九宫格进行遍历 for(int i=row_new; i<row_new+3; ++i){ for(int j=column_new; j<column_new+3; ++j){ if(board[i][j] == str_i) return true; } } return false; }
所有的代码如下所示:
class Solution { public: void solveSudoku(vector<vector<char>>& board) { backtrack(board, 0); } bool backtrack(vector<vector<char>>& board, int index){ if(index == 9*9) return true; int row = index / 9; int column = index % 9; for(int i=1; i<=9; ++i){ // 每个方格上遍历放九个数字 if(board[row][column] != '.'){ // 给定行和列上已经有数字后,进行剪枝,这一剪枝条件放到for循环外面也是可以的 return backtrack(board, index+1); } if(Pruning(board, row, column, i+'0')){ continue; } board[row][column] = i+'0'; if(backtrack(board, index+1)) return true; board[row][column] = '.'; } return false; } bool Pruning(vector<vector<char>>& board, int row, int column, char str_i){ //if(board[row][column] != '.') return true; for(int i=0; i<9; ++i){ if(board[row][i] == str_i) return true; // 对列遍历,剪枝 if(board[i][column] == str_i) return true; // 对行遍历,剪枝 } int row_new = (row/3)*3, column_new = (column/3)*3; // 对小的九宫格进行遍历 for(int i=row_new; i<row_new+3; ++i){ for(int j=column_new; j<column_new+3; ++j){ if(board[i][j] == str_i) return true; } } return false; } };
参考
- https://leetcode-cn.com/problems/subsets/solution/c-zong-jie-liao-hui-su-wen-ti-lei-xing-dai-ni-gao-/
- 回溯算法
- 回溯算法套路详解