算法:皇后问题

问题

国际象棋中的皇后,可以横向、纵向、斜向移动。如何在一个NXN的棋盘上放置N个皇后,使得任意两个皇后都不在同一条横线、竖线、斜线方向上?

举个栗子,下图的绿色格子是一个皇后在棋盘上的“封锁范围”,其他皇后不得放置在这些格子:
在这里插入图片描述

递归回溯法

所谓递归回溯,本质上是一种枚举法。这种方法从棋盘的第一行开始尝试摆放第一个皇后,摆放成功后,递归一层,再遵循规则在棋盘第二行来摆放第二个皇后。如果当前位置无法摆放,则向右移动一格再次尝试,如果摆放成功,则继续递归一层,摆放第三个皇后……

如果某一层看遍了所有格子,都无法成功摆放,则回溯到上一个皇后,让上一个皇后右移一格,再进行递归。如果八个皇后都摆放完毕且符合规则,那么就得到了其中一种正确的解法。

看个例子:把皇后

1.第一层递归,尝试在第一行摆放第一个皇后:
在这里插入图片描述

2.第二层递归,尝试在第二行摆放第二个皇后(前两格被第一个皇后封锁,只能落在第三格):
在这里插入图片描述

3.第三层递归,尝试在第三行摆放第三个皇后(前四格被第一第二个皇后封锁,只能落在第五格):

在这里插入图片描述
4.第四层递归,尝试在第四行摆放第四个皇后(第一格被第二个皇后封锁,只能落在第二格):
在这里插入图片描述

5.第五层递归,尝试在第五行摆放第五个皇后(前三格被前面的皇后封锁,只能落在第四格):
在这里插入图片描述
6.由于所有格子都“绿了”,第六行已经没办法摆放皇后,于是进行回溯,重新摆放第五个皇后到第八格。:
在这里插入图片描述
7.第六行仍然没有办法摆放皇后,第五行也已经尝试遍了,于是回溯到第四行,重新摆放第四个皇后到第七格。
在这里插入图片描述
8.继续摆放第五个皇后,以此类推……

看个例子:四皇后

现在我们把第一个皇后放在第一个格子,被涂黑的地方是不能放皇后的
在这里插入图片描述

第二行的皇后只能放在第三格或第四格,比如我们放在第三格:
在这里插入图片描述
这样一来前面两位皇后已经把第三行全部锁死了,第三位皇后无论放在第三行的哪里都难逃被吃掉的厄运。于是在第一个皇后位于第一格,第二个皇后位于第三格的情况下此问题无解。所以我们只能返回上一步,来给2号皇后换个位置:

在这里插入图片描述

此时,第三个皇后只有一个位置可选。当第三个皇后占据第三行蓝色空位时,第四行皇后无路可走,于是发生错误,则返回上层调整3号皇后,而3号皇后也别无可去,继续返回上层调整2号皇后,而2号皇后已然无路可去,则再继续返回上层调整1号皇后,于是1号皇后往后移一格位置如下,再继续往下安排:
在这里插入图片描述

建模:抽象成N叉树

在这里插入图片描述

从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。

那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。

题目

51. N皇后:所有的解决方案

题目来源

题目描述

在这里插入图片描述

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {

    }
};

题目解析

(1)递归函数参数

  • 定义两个全局二维遍历result来记录最终结果
  • 参数n是棋盘的大小,row表示当前遍历到棋盘的第几层
vector<vector<string>> result;
void backtracking(int n, int row, vector<string>& chessboard) {

(2)递归函数参数

  • 从上面可以看出,当递归到棋盘的最底层(也就是叶子节点)时,就可以收集结果并返回了
if (row == n) {
    result.push_back(chessboard);
    return;
}

(3)单层搜索的逻辑

  • 递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。
  • 每次都是要从新的一行的起始位置开始搜,所以都是从0开始。
for (int col = 0; col < n; col++) {
    if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
        chessboard[row][col] = 'Q'; // 放置皇后
        backtracking(n, row + 1, chessboard);
        chessboard[row][col] = '.'; // 回溯,撤销皇后
    }
}

(4)验证是否合法

  • 皇后约束:
    • 不能同行
    • 不能同列
    • 不能同斜线 (45度和135度角)

代码如下:

class Solution {
    vector<vector<string>> ans;
    bool isValid(int row, int col, vector<string>& chessboard, int n) {
        int count = 0;
        // 检查列
        for (int i = 0; i < row; i++) { // 这是一个剪枝
            if (chessboard[i][col] == 'Q') {
                return false;
            }
        }
        // 检查 45度角是否有皇后(往左上看)
        for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        // 检查 135度角是否有皇后(往右上看)
        for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }
    void dfs(int n, int row,  std::vector<std::string> &chessboard){
        if(row == n){
            ans.push_back(chessboard);
            return;
        }

        for (int col = 0; col < n; ++col) {
            if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
                chessboard[row][col] = 'Q'; // 放置皇后
                dfs(n, row + 1, chessboard);
                chessboard[row][col] = '.'; // 回溯,撤销皇后
            }
        }
    }
public:
    vector<vector<string>> solveNQueens(int n) {
       std::vector<std::string> chessboard(n, std::string(n, '.'));
       dfs(n, 0, chessboard);
        return ans;
    }
};

为什么没有在同行进行检查呢?因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。

52. N皇后 II:解决方案有几种

题目来源

题目描述

在这里插入图片描述

class Solution {
public:
    int totalNQueens(int n) {

    }
};

题目解析

暴力递归

(1)思路

  • 如果在(i, j)位置(第i行第j列)放置了一个皇后,接下来哪些位置不能放皇后呢?
    1. 整个第i行都不能放皇后
    2. 整个第j列都不能放皇后
    3. 如果位置(a, b)满足|a - i| == |b - j|,说明(a, b)与(i, j)处在同一条斜线上,也不能放皇后

(2)实现

  • 把递归过程直接设计成逐行放置皇后的方式,可以避开条件1的那些不能放置的位置
  • 用一个数组保存已经放置的皇后位置,假设数组为record, r e c o r d [ i ] record[i] record[i]的值表示第 i i i行皇后所在的列数。在递归计算到第i行第j列时:
    • 先看 r e c o r d [ 0... k ] ( k < i ) record[0...k](k < i) record[0...k](k<i)的值,看是否有 j j j相等的值,如果有,说明不能放置皇后;
    • 再看是否有 ∣ k − i ∣ = = ∣ r e c o r d [ k ] − j ∣ |k - i| == |record[k] - j| ki==record[k]j,如果有,也不能放置皇后

(3)代码

class Solution {
    bool isValid(std::vector<int>& record, int i, int j){
        for (int k = 0; k < i; ++k) {
            if(record[k] == j || std::abs(k - i) == std::abs(record[k] - j)){
                return true;
            }   
        }
        return false;
    }

	// 当前来到i行,一共是0~N-1行
	// 在i行上放皇后,所有列都尝试
	// 必须要保证跟之前所有的皇后不打架
	// int[] record record[x] = y 之前的第x行的皇后,放在了y列上
	// 返回:不关心i以上发生了什么,i.... 后续有多少合法的方法数
   int process(int i, std::vector<int>& record, int n){
       if(i == n){
           return 1;
       }

       int way = 0;
       for (int j = 0; j < n; ++j) {  //每个皇后有N种选择
           if(isValid(record, i, j)){  //每次放之前都看一下这个位置能不能放
               record[i] = j;
               way += process(i + 1, record, n);
           }
       }
       return way;
   }
public:
    int ways(int n){
        if(n < 1){
            return 0;
        }
        
        std::vector<int> record(n);
        return process(0, record, n);
    }
};

还可以使用位运算来加速(最优解)。

class Solution {
    // 7皇后问题
    // limit : 0....0 1 1 1 1 1 1 1,这个变量的值在递归过程中是始终不变的
    // 之前皇后的列影响:colLim,即递归计算到上一行为止,在哪些列上已经放置了皇后
    // 之前皇后的左下对角线影响:leftDiaLim。举个例子
    // 之前皇后的右下对角线影响:rightDiaLim
   int process2(int limit, int colLim, int leftDiaLim, int rightDiaLim) {
        if (colLim == limit) {
            return 1;
        }
        // pos中所有是1的位置,是你可以去尝试皇后的位置
        int pos = limit & (~(colLim | leftDiaLim | rightDiaLim));
        int mostRightOne = 0;
        int res = 0;
        while (pos != 0) {
            mostRightOne = pos & (~pos + 1);
            pos = pos - mostRightOne;
            res += process2(limit, colLim | mostRightOne, (leftDiaLim | mostRightOne) << 1,
                            (unsigned)(rightDiaLim | mostRightOne) >> 1);  //无符号右移
        }
        return res;
   }
public:
    int ways(int n){
        // 因为本方法位运算的载体是int型变量,所以该方法只能算1~32皇后问题,如果想要更多皇后,需要使用包含更多位的变量
        if (n < 1 || n > 32) {
            return 0;
        }
        // limit 表示当前行哪些位置可以放皇后,1表示可以放皇后,0表示不可以放皇后
        // 比如8皇后, 初始化limit为0000000.....000011111111(limit 最右8个1,其他都是0)
        // 比如32皇后,初始化limit为111111...1111(32个1)
        int limit = n == 32 ? -1 : (1 << n) - 1;
        std::vector<int> record(n);
        return process2(0, record, n);
    }
};

回溯法 + set集合

(1)思路

  • 正对角线(从左上到右下)的特点:横纵坐标之差相同
  • 副对角线(从右上到左下)的特点:横纵坐标之和相同
  • 每行、每列及正副对角线上只能出现一个皇后,由于从上到下放置皇后,所以只需考虑当前坐标对应的列和正副对角线是否已被访问即可

(2)定义变量

  • i:横坐标
  • j:纵坐标
  • col:set集合,存储遍历过的列j
  • pos:set集合,存储正对角线之差i-j
  • neg:set集合,存储副对角线之和i+j

(3)步骤

  • 做选择:如果该坐标上对应的列和正副对角线上没有皇后,放置皇后并在对应集合中标记
  • 递归:进入下一行
  • 撤销选择:将当前标记从三个集合中删除

(4)复杂度分析

  • 时间复杂度:O(N!)
  • 空间复杂度:O(N)

(5)代码

class Solution {
    int count = 0;
    std::map<int, int> map1;  //正斜线是否被占用
    std::map<int, int> map2; //反斜线是否被占用
    std::vector<int> col;    //列是否被占用

    void dfs(int i, int n){
        if(i == n){  //放完了所有的皇后
            ++count;
            return;
        }

        for (int j = 0; j < n; ++j) {  //放第idx行,第j列
            if(col[j] == 0 && map1.count(i - j) == 0 && map1.count(i + j) == 0){  //放第n个皇后,第j列是否可放?
                col[j] = 1;
                map1[i - j] = 1;
                map1[i + j] = 1;
                dfs(i + 1, n);  //递归放下一个皇后
                col[j] = 0;
                map1[i - j] = 0;
                map1[i + j] = 0;
            }
        }

    }
public:
    int totalNQueens(int n) {
        if(n <= 1){
            return n;
        }

        col.resize(n);
        dfs(0, n);
        return count;
    }
};
回溯法+位运算

(1)思路

  • N个位置可以对应成N个二进制位
  • 二进制位状态:
    • 0:无皇后,可以选择
    • 1:有皇后,不可选择
  • 比如八皇后第一行状态为0000 0000,当第二位被选择后该行的状态变成了0100 0000,下一行同样第二位不能被选,正对角线对应的第三位不能被选(对应当前行右移了一位),副对角线对应的第一位不能被选(对应当前行左移了一位)
    • 已选列的二进制表示:0100 0000
    • 已选正对角线的二进制表示:0010 0000
    • 已选副对角线的二进制表示:1000 0000

(2)定义变量

  • i:横坐标
  • j:纵坐标
  • col:已选列的二进制位
  • pos:已选正对角线的二进制位
  • neg:已被副对角线的二进制位

(3)步骤

  • 将col、pos和neg做或运算(|)得到的二进制位pre中,1表示不能被选的位置,0表示可以被选的位置
  • 遍历N位二进制,如果当前可被选择,则将该位置加入到对应二进制中,同时正对角线pos右移一位,副对角线neg左移一位
  • 递归执行上述操作,直到所有选择执行完成

(4)复杂度分析

  • 时间复杂度:O(N!)
  • 空间复杂度:O(N)

(5)代码


面试题 08.12. N皇后:打印所有的方案

题目来源

题目描述

在这里插入图片描述

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {

    }
};

题目解析

思路:我们把这个问题分成n个阶段,依次将n个棋子放到第一行、第二行、第三行…。在放置的过程中,我们不停的检查当前的方法是否满足要求。如果满足,就跳到下一行继续放置棋子;如果不满足,就换一种方法,继续尝试


可视化过程

分析

解决八皇后问题,可以分为两个层面:

1.找出第一种正确摆放方式,也就是深度优先遍历。
2.找出全部的正确摆放方式,也就是广度优先遍历。

我们先来找出第一种正确摆放方式。

先解决几个问题

(1)国际象棋的棋盘如何表示?

用一个长度为9的二维数组即可

const int MAX_NUM = 8;
const chessBoard[] = new int[MAX_NUM][MAX_NUM] 

由于这里使用的是int数组,int的初始值是0,代表没有落子。当有皇后放置的时候,对应的元素值改为1。

在这里,二维数组的第一个维度代表横坐标,第二个维度代表纵坐标,并且从0开始。比如chessBoard[3][4]代表的是棋盘第四行第五列格子的状态。

golang实现

package main

import "fmt"

const N = 4;

// 第1个N是由几行, 第2个N是有几列
var queue [N][N]int

func show()  {
	for i := 0; i < N;  i++ { // 第i行
		for j := 0; j < N ; j++ {
			//fmt.Printf("%5d", queue[i][j])
			if queue[i][j]==1{
				fmt.Printf("%5s","■")
			}else{
				fmt.Printf("%5s","□")
			}
		}
		fmt.Println()
	}
	fmt.Println()
}


// 判断 第row行,第col列是否可以存放数据
func judge(row, col int) bool  {
	for i := 0; i < col;  i++ {  //当前列是否已经摆放皇后
		if queue[row][i] == 1 { // 行固定, 前j列是否已经存放数据
			return false
		}
	}

	for j := 0; j < col; j++{  //当前行是否已经摆放皇后
		if queue[row][j] == 1 { // 列固定, 前j行是否已经存放数据
			return false
		}
	}

	// 左上角是否已经摆放皇后
	for i, j := row - 1, col - 1; i >= 0 && j >= 0 ; i, j = i - 1, j - 1{
		if queue[i][j] == 1 {
			return false
		}
	}

    // 右上角是否已经摆放皇后
	for i, j := row - 1, col + 1; i >= 0 && j <= N - 1 ; i, j = i - 1, j + 1{
		if queue[i][j] == 1 {
			return false
		}
	}

   // 左下角是否已经摆放皇后
	for i, j := row + 1, col - 1; i <= N - 1 && j >= 0 ; i, j = i + 1, j - 1{
		if queue[i][j] == 1 {
			return false
		}
	}

   //右下角是否已经摆放皇后
	for i, j := row - 1, col + 1; i >= 0 && j <= N - 1 ; i, j = i - 1, j + 1{
		if queue[i][j] == 1 {
			return false
		}
	}

	return true
}

var count = 0;
/*
 * num 的意思是:我们正在放置第i个皇后
*/
func find_queue(num int)  {
	//show()
	if num == N { // 所有的皇后都放完了
		count++ // 那自然是找到了一种解法,于是八皇后问题解法数加1
		show()
		return
	}

	// 如果当前还没排到第八行,则遍历所有列col,将当前col存储在数组c里,然后使用judge()检查row行col列能不能摆皇后,若能摆皇后,则递归调用queen去安排下一列摆皇后的问题。
	for row := 0;row < N ; row++ { 	// 第num个皇后放到第num列[固定好的],[0, N)行上试探
		if judge(row, num) {   // 判断当前位置是否可以摆放皇后
			queue[row][num] = 1  //摆放皇后
			find_queue(num + 1) //摆放下一个皇后
			queue[row][num] = 0 // 只有在皇后摆不下去的时候会执行清0的动作(避免脏数据干扰),如果皇后摆放很顺利的话从头到尾是不会走这个请0的动作的,因为已经提前走if里面的return方法结束了。
		}
		//show()
	}
}
func main() {
	find_queue(0)

}

参考

在这里插入图片描述

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值