回溯-N皇后
回溯算法其实就是暴力穷举算法,只不过暴力穷举采用了先拆解子问题,然后将子问题使用递归的方式进行求解,并且在不满足条件的情况下,有向上回溯的过程。
许多复杂的,规模较大的问题都可以使用回溯法。
回溯问题看起来比较复杂,但一般都有固定的套路。
据我个人的理解,回溯过程中需要明确以下几个问题
1.什么是合法的解
2.什么时候退出递归
3.什么时候需要回溯
下面我们使用经典的N皇后问题,来解读回溯算法的一般思路。
案例1:
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
现在我们来依次解决前面提出的三个问题
1.什么是N皇后问题合法的解
这个问题可以分解成两个问题,每一步骤的合法解和全局的合法解
每一步骤的合法解其实题意中已经非常明确,n皇后放在nxn的棋盘,要求同一行、同一列、同一斜线都只能有一个皇后。
我们是按照每行一个皇后开始放置的,每一行肯定不会重复,我们只需要校验每一列、每一个45度斜线、每一个135度斜线没有重复的皇后即可。
我们定义三个set来判断列和两个斜线上的皇后数
//列 private Set<Integer> columnSet = new HashSet<>(); //45度上升对角线 private Set<Integer> diagonalSet1 = new HashSet<>(); //135度下降对角线 private Set<Integer> diagonalSet2 = new HashSet<>();
列的表示法很直观,一共有 N列,每一列的下标范围从 0 到 N−1,使用列的下标即可明确表示每一列。
如何表示两个方向的斜线呢?对于每个方向的斜线,需要找到斜线上的每个位置的行下标与列下标之间的关系。
我们发现
135度的斜线为从左上到右下方向,同一条斜线上的每个位置满足行下标与列下标之差相等
45度的斜线为从左下到右上方向,同一条斜线上的每个位置满足行下标与列下标之和相等。
如果每个步骤的解都合法,那么当最后一行的皇后放好之后,就是全局的合法解
2.什么时候退出递归
这里的深度优先遍历使用的是递归,一定要有明确的退出递归的方式,不然就会Stack Overflow!
在本题目,当我们从第一行开始放皇后,放到最后一行结束的时候,就得到了一个全局合法解,此时就应该退出递归。
3.什么时候需要回溯
需要回溯的情况有两种,一种是第二个问题我们找到合法解的时候,有一个明确的return。
还有一个是隐含的递归退出,就是在当前方法中,遍历了所有可能的值都不是可行解,此时就会从for循环结束,接下来就会从方法栈退出。
好了,核心问题都解决了,我们上代码。
class Solution { List<List<String>> res = new ArrayList<>(); private Set<Integer> columnSet = new HashSet<>(); //45度上升对角线 private Set<Integer> diagonalSet1 = new HashSet<>(); //135度下降对角线 private Set<Integer> diagonalSet2 = new HashSet<>(); /** * n皇后问题 * * @param n * @return */ public List<List<String>> solveNQueens(int n) { //初始化一个nxn空棋盘,一开始每个格子都为逗号 char[][] chessboard = new char[n][n]; for (char[] c : chessboard) { Arrays.fill(c, '.'); } //从第一行开始回溯 dfs(n, 0, chessboard); return res; } public void dfs(int n, int row, char[][] chessboard) { //如果row==n,说明问题解决了 if (row == n) { res.add(Array2List(chessboard)); return; } for (int col = 0; col < n; ++col) { //对应某一行row的某列col,如果可以填入Q,则填入,接下来进行row+1行操作 //否则取消刚刚的操作,尝试col+1列 if (isValid(row, col)) { chessboard[row][col] = 'Q'; columnSet.add(col); diagonalSet1.add(row + col); diagonalSet2.add(row - col); dfs(n, row + 1, chessboard); chessboard[row][col] = '.'; columnSet.remove(col); diagonalSet1.remove(row + col); diagonalSet2.remove(row - col); } } } private List<String> Array2List(char[][] chessboard) { List<String> list = new ArrayList<>(); for (int i = 0; i < chessboard.length; i++) { char[] chars = chessboard[i]; list.add(new String(chars)); } return list; } /** * 判断皇后位置是否合法? * 要求列没有Q,两个对角线都没有Q * * @param row * @param col * @return */ public boolean isValid(int row, int col) { return !columnSet.contains(col) && !diagonalSet1.contains(row + col) && !diagonalSet2.contains(row - col); } }