回溯-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);
    }
}

 

posted @ 2022-11-01 17:30  Mars.wang  阅读(72)  评论(0编辑  收藏  举报