经过n皇后问题搞明白回溯算法

前言

很久没聊算法啦!此次咱们来聊聊n皇后问题。n 皇后问题,研究的是如何将 n 个皇后放置在 n×n 的棋盘上,而且使皇后彼此之间不能相互攻击。好多同窗对这样的问题都比较慌张,以为规则多烧脑抗拒,祈祷面试中不要遇到,别急,咱们今天就来尝试把这其中的逻辑给说道说道。面试

一个皇后能够向水平、垂直以及向斜对角方向移动,若是一个皇后出如今另外一个皇后的同一行,同一列或者斜对角,那它就能够被这个皇后攻击。算法

那咱们直觉里的一个比较粗暴的办法,就是列举出在棋盘上全部的状况,而后咱们判断每一个组合是否是符合咱们的条件。可是实际上咱们不须要尝试全部的组合,咱们知道当咱们在某一列上放置了一个皇后以后,其它的皇后就不能放在这一列了,在它的同一个水平线上跟四个斜对角也放不了。这样咱们能够最先发现“此路不通”。数组

当咱们发现一条路行不通时,咱们赶忙回到前面一步,而后尝试另一个不一样的选择,咱们称之为回溯安全

这个高大上的回溯是什么

针对n皇后问题咱们把这个思路再展开一下:markdown

  1. 把一个皇后放在第一行的第一列
  2. 而后咱们在第二行找到一个位置,在这儿第二个皇后不会被第一行的皇后攻击到
  3. 若是咱们找不到这样的一个位置, 那咱们就回退到前一行,尝试把这个皇后放到那一行的下一列
  4. 重复这个步骤,直到咱们在最后一行也找到一个合适的位置放置最后一个皇后,那这时咱们就找到了一种解决方案
  5. 找到一个解决方案以后,咱们会继续回退到前一行,去尝试找到下一个解决方案

是否是还以为有点抽象?函数

那咱们拿其中一个场景来具体说说:优化

棋盘,X表明一个皇后

  1. 咱们从x=0,y=0开始,第一个皇后a放在这儿是安全的,
  2. 而后第二行的皇后b为了不被攻击,只能从第三列开始放
  3. 那此时第三行咱们发现就无法儿放皇后了,由于无论放哪儿都会被皇后a或者皇后b攻击
  4. 那咱们只能回溯到第二行,继续日后找一个合适的列来放置皇后b
  5. 当第二行找到最后一列也不知足的条件时,咱们只能回溯到第一行,继续日后找能够放置皇后a的列,重复这个过程

走两步?

如今是否是以为眼睛会了?🤔,接下来咱们可让手来试试了。spa

首先咱们须要一个方法来判断某一个位置能不能放皇后,这样若是一个位置会被棋盘上已有的皇后攻击的话,咱们能够直接跳过这个位置:code

// 这个方法用来判断咱们接下来要放置的皇后在不在某个已经放置的皇后的水平方向、垂直方向或者斜对角,
    // 若是都不,那咱们找到了一个合适的位置来放一个新皇后了
    static boolean isValidPosition(int proposedRow, int proposedCol, List<Integer> solution) {
        //对当前棋盘上的全部皇后,咱们都要作判断
        for (int oldRow = 0; oldRow < proposedRow; ++oldRow) {
            int oldCol = solution.get(oldRow);
            int diagonalOffset = proposedRow - oldRow;
            if (oldCol == proposedCol ||
                    oldCol == proposedCol - diagonalOffset ||
                    oldCol == proposedCol + diagonalOffset) {
                return false;
            }
        }
        return true;
    }
复制代码

有了这个方法以后,咱们就须要实现逐行搜索以及在全部路不通时回到前一行的搜索里面来,继续寻找其它可能性。每一行的搜索方式都一致,因此这边适合使用递归来实现咱们的逻辑:orm

static void solveNQueensRec(int n, List<Integer> solution, int row, List<List<Integer>> results) {
        // 最后一行也找到了一个合适的位置,咱们成功找到了一种解决方案
        if (row == n) {
            results.add(new ArrayList<Integer>(solution));
            return;
        }

        // 从每一行的第一列开始尝试
        for (int i = 0; i < n; ++i) {
            // 对于走到最后一列还没都没有找到合适的点的状况, 当前递归结束,调用栈回到上一层的递归流程,会回去执行前面一行里剩余的状况
            if (isValidPosition(row, i, solution)) {
                solution.set(row, i);
                solveNQueensRec(n, solution, row + 1, results);
            }
        }
    }
复制代码

当有条路走不通时,调用栈里当前递归就执行结束了,它会回到上一个递归的调用逻辑里,也就实现了咱们的回溯。咱们的目的很简单,这一行走到最后没路走了,就继续回到前一行继续日后走,直到全部的路都尝试过。

最后执行一下咱们的递归函数就好啦:

static int solveNQueens(int n, List<List<Integer>> results) {
        List<Integer> solution = new ArrayList<Integer>(n);
        for (int i = 0; i < n; ++i) {
            solution.add(-1);
        }
        solveNQueensRec(n, solution, 0, results);
        return results.size();
    }
复制代码

这边至关于从N×N的数组里,选出N个数,时间复杂度是O(n^n),而空间复杂度是O(n!)

继续发散

上面咱们搜索的过程当中,一行一行上升去寻找合适的位置,而后在某个条件下又回到前一行,有点像栈的入栈出栈操做,其实咱们也是能够用栈来实现整个回溯过程的。咱们在某一行里找到一个合适的位置时就把它的列push到栈中,回溯到前一行时再把它pop出来。

// 这边栈里咱们只存放列的值
    static int solveNQueens(int n, List<List<Integer>> results) {

        List<Integer> solution = new ArrayList<Integer>(n);
        Stack<Integer> solStack = new Stack<Integer>();

        for (int i = 0; i < n; ++i) {
            solution.add(-1);
        }

        int row = 0;
        int col = 0;

        while(row < n){
            while(col < n){
                // 当前能够放置皇后,继续下一行的寻找
                if(isValidPosition(row, col, solution)){
                    solStack.push(col);
                    solution.set(row, col);
                    row++;
                    col = 0;
                    break;
                }
                // 当前位置不行,尝试在下一列放置皇后
                col++;
            }

            // 找到了当前行的最后一列
            if(col == n){
                // 说明前面一行还没到最后一列,执行pop操做回到前一行寻找过程
                if(!solStack.empty()){
                    col = solStack.peek() + 1;
                    solStack.pop();
                    row--;
                }
                else{
                    // 只有第一行也走到了最后一列,而且全部路径都尝试过了,此时因为上面if里的逻辑栈空了,说明咱们的寻找过程该结束了
                    break;
                }
            }

            if(row == n){
                // 咱们找到一种符合条件的摆放位置
                results.add(new ArrayList<Integer>(solution));

                // 回溯到前一行
                row--;
                col = solStack.peek() + 1;
                solStack.pop();
            }
        }
        return results.size();
    }
复制代码

时间复杂度是O(n^n),空间复杂度是O(n!)。这边整个逻辑仍是比较直接的,咱们依旧须要isValidPosition这个辅助方法来判断某个位置能不能放置皇后,而后对每一个位置逐一判断,用栈来配合寻找过程当中的回溯操做,核心思想仍是不变的。

总结

咱们两种办法里都把全部的符合规则的摆放记录下来了,若是咱们只须要最后求得有多少种可能性,那咱们其实能够把数组换成一个变量来计数,这样咱们的空间复杂度能够优化成O(n)

好啦,相信你们这会儿对回溯算法有了一个感性的认识,也能明白回溯只是咱们面对问题时常规的思路,并非什么高大上的概念,咱们不用去畏惧它~

相关文章
相关标签/搜索