N-皇后问题的演进(附代码)


title: N 皇后问题的演进(附代码)
date: 2020-03-18 11:49:45
tags: Algorithm


N-皇后问题的演进(附代码)

一、该类问题的通用回溯解法

N 皇后问题的解法是典型的回溯算法,回溯算法本质上就是穷举决策树。暴力有效。

回溯问题的解法框架:

res = []
def backtrack(path, choices):
	if 满足终止条件:
		res.append(path[:])
		return
	for choice in choices:
		做选择
		backtrack(path, choices)
		撤销选择

关于这类回溯问题如何解,推荐这篇文章回溯算法解题套路框架

二、N 皇后问题两个核心问题

N 皇后问题 有两个需要关键处理的:

  1. 回溯,可以直接套模版
  2. 合法的放置皇后,判断位置的是否合法

如何判断位置是否合法

该部分主要参考 力扣上的这篇帖子:Accepted 4ms c++ solution use backtracking and bitmask, easy understand.

如何算合法放置?

image-20200318120331036

N个皇后,要求放置时不同行,不同列,不同对角线,如果算法是遍历每行(当然也可以遍历每列,并确保每行只有一个皇后,那么只需考虑,不同列,和不同对角线)

关于对角线部分,其实只需考虑 45度方向(即左上方)135度方向(即右上方),因为递归遍历每行时,当到达某行时,未遍历的行数皆没有放置皇后。

故可以得出代码(直接放原博主的C++代码):

class Solution {
public:
    std::vector<std::vector<std::string> > solveNQueens(int n) {
        std::vector<std::vector<std::string> > res;
        std::vector<std::string> nQueens(n, std::string(n, '.'));
        solveNQueens(res, nQueens, 0, n);
        return res;
    }
private:
    void solveNQueens(std::vector<std::vector<std::string> > &res, std::vector<std::string> &nQueens, int row, int &n) {
        if (row == n) {
            res.push_back(nQueens);
            return;
        }
        for (int col = 0; col != n; ++col)
            if (isValid(nQueens, row, col, n)) {
                nQueens[row][col] = 'Q';
                solveNQueens(res, nQueens, row + 1, n);
                nQueens[row][col] = '.';
            }
    }
    bool isValid(std::vector<std::string> &nQueens, int row, int col, int &n) {
        //check if the column had a queen before.
        for (int i = 0; i != row; ++i)
            if (nQueens[i][col] == 'Q')
                return false;
        //check if the 45° diagonal had a queen before.
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j)
            if (nQueens[i][j] == 'Q')
                return false;
        //check if the 135° diagonal had a queen before.
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; --i, ++j)
            if (nQueens[i][j] == 'Q')
                return false;
        return true;
    }
};

这样在判断皇后位置是否合法时,其实有很多冗余计算,不妨以空间换时间,通过数组存下位置的状态(是否可以放置皇后),原博主天才的想法:

对于 n皇后,总共有n列,45° 对角线数目:2*n-1, 对角线135°的数目也为2 * n-1。当到达[row,col]时,列号为col,则 45°对角线编号为row + col,而135°对角线编号为n-1 + col- row。 我们可以使用三个数组来指示列或对角线之前是否有王后,如果没有,我们可以将王后放在这个位置并继续。

示意图:

image-20200318121504700

代码实现(原博主的C++代码):

/**    | | |                / / /             \ \ \
  *    O O O               O O O               O O O
  *    | | |              / / / /             \ \ \ \
  *    O O O               O O O               O O O
  *    | | |              / / / /             \ \ \ \ 
  *    O O O               O O O               O O O
  *    | | |              / / /                 \ \ \
  *   3 columns        5 45° diagonals     5 135° diagonals    (when n is 3)
  */
class Solution {
public:
    std::vector<std::vector<std::string> > solveNQueens(int n) {
        std::vector<std::vector<std::string> > res;
        std::vector<std::string> nQueens(n, std::string(n, '.'));
        std::vector<int> flag_col(n, 1), flag_45(2 * n - 1, 1), flag_135(2 * n - 1, 1);
        solveNQueens(res, nQueens, flag_col, flag_45, flag_135, 0, n);
        return res;
    }
private:
    void solveNQueens(std::vector<std::vector<std::string> > &res, std::vector<std::string> &nQueens, std::vector<int> &flag_col, std::vector<int> &flag_45, std::vector<int> &flag_135, int row, int &n) {
        if (row == n) {
            res.push_back(nQueens);
            return;
        }
        for (int col = 0; col != n; ++col)
            if (flag_col[col] && flag_45[row + col] && flag_135[n - 1 + col - row]) {
                flag_col[col] = flag_45[row + col] = flag_135[n - 1 + col - row] = 0;
                nQueens[row][col] = 'Q';
                solveNQueens(res, nQueens, flag_col, flag_45, flag_135, row + 1, n);
                nQueens[row][col] = '.';
                flag_col[col] = flag_45[row + col] = flag_135[n - 1 + col - row] = 1;
            }
    }
};

其实完全没有必要开辟三个数组,一个flag数组就够。现在,当到达[row,col]时,列的下标为col,对角线45°的下标为n + row + col,对角线135°的下标为n + 2 * n-1 + n-1 + col -行。flag 数组的大小为 n+2n-1+2n-1=5n-2

代码实现(python3):

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        def solveNQ(n, row, res, flag):
            if row==n:
                res.append([str for str in nq])
                return
            for col in range(n):
                if flag[col] and flag[n+row+col] and flag[4*n-2+col-row]:
                    flag[col]=flag[n+row+col]=flag[4*n-2+col-row]=0
                    nq[row] = nq[row][:col]+'Q'+nq[row][col+1:]
                    solveNQ(n, row+1, res, flag)
                    flag[col]=flag[n+row+col]=flag[4*n-2+col-row]=1
                    nq[row] = nq[row][:col]+'.'+nq[row][col+1:]
        nq = ['.'*n for _ in range(n)]
        res =[]
        flag = [1 for _ in range(5*n-2)]
        solveNQ(n, 0, res, flag)
        return res

但是python中修改字符串其实挺浪费时间的,完全可以存一个列号,等求出所有的排列后,再构造出棋盘分布。

代码如下:

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        def solveNQ(n, row, res, flag, nqCol):
            if row==n:
                res.append(nqCol[:])
                return
            for col in range(n):
                if flag[col] and flag[n+row+col] and flag[4*n-2+col-row]:
                    flag[col]=flag[n+row+col]=flag[4*n-2+col-row]=0
                    nqCol[row] = col
                    solveNQ(n, row+1, res, flag, nqCol)
                    flag[col]=flag[n+row+col]=flag[4*n-2+col-row]=1
                    nqCol[row] = -1
        res =[]
        flag = [1 for _ in range(5*n-2)]
        solveNQ(n, 0, res, flag, [-1 for _ in range(n)])    
        for i, npCol in enumerate(res):
            board =[]
            for col in npCol:
                board.append('.'*col+'Q'+'.'*(n-1-col))
            res[i] = board   
        return res

17-21行代码是构造棋盘分布的

总结

  1. 回溯问题要学会套模板,
  2. python 代码最好尽可能解耦合,模块化,降低模块的耦合性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值