最简单最详细的n皇后问题讲解(回溯法)

最简单最详细的n皇后问题讲解(回溯法)

思路

51. N 皇后 - 力扣(LeetCode)

这是最经典的回溯法问题,但是通过我本人的理解,我感觉就像是一个复杂一点的枚举法,下面我会把思考思路和解题思路都详细得讲解出来。

贴上问题描述,更好得去讲解一下:

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击(每一行都要有一个皇后)。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

问题描述按照我的理解就是皇后的左,左上,中上,右上,右,右下,中下,左下的八个方向的延申都不能有另外的皇后,那这样的话可以看成一个扩散问题,一个皇后落地,就有多少的位置被占据而不能放置新的皇后,然后枚举还有哪些位置能放皇后的,一次次得去循环枚举。
在这里插入图片描述回溯一般是指这一步骤试错了,就回到上一个步骤的状态,再尝试,不行的话,再回到上一次尝试,无线循环。那这样的话,能不能把这个步骤理解为不断地去枚举呢:枚举一个一个的步骤,一个步骤不通,就舍弃一个步骤,取有效的步骤的结果。

所以最终思路就可以理解为: 我从第一行的第一个位置开始遍历起,枚举第一行所有皇后的放置情况,然后通过递归再遍历第二行一直到最后一行的皇后放置情况,如果直到最后一行都能放置,就说明这个方法是有效的,就保存起来

在这里插入图片描述

具体代码实现思路

1.初始化数据

首先我们需要定义基本的数据结构,主函数:

1.n是皇后的数量

2.k用来递归每一行,每次递归k+1代表遍历的行,数组[x行] [y列]

3.attack用来存储被皇后覆盖到的区域,被覆盖到为1,没有覆盖到的区域为0

4.vector queens 存储皇后的位置每一个string元素代表一行,vector代表每一列

5.vector<vector> solve; 用来存储皇后的位置结果

6.backtarck(attack, queens, solve, k, n); 最后我们将参数传到回溯函数里面去

    vector<vector<string>> solveNQueens(int n) {
        int k = 0;
        vector<vector<int>> attack(n, vector<int>(n, 0)); // 分配内存并初始化为零
        vector<string> queens(n, string(n,'.')); // 分配内存并每个元素初始化为(n)个点号
        vector<vector<string>> solve; //存储结果
        backtarck(attack, queens, solve, k, n); //递归函数
        return solve;
    }

2.完成主体部分的设计

定义完基本的数据之后,我们直接开始递归函数的设计,递归函数的最主要目的是直接能得到返回的结果,中间可能会涉及到需要再单独处理的函数,但我们写算法的时候,可以先不去实现具体的功能,我们先把 主体的部分写清楚之后再去完善细节部分:

思路为:
1.首先我们考虑到的情况是从第一行开始遍历到最后,只要能递归到最后的,都是正确的方法,所以一开始,我们先将当k==n的情况,记录答案,**注意:**一定要写return不然此时k此时已经在了n+1行(从1算起的说法)会爆栈,所以一定要返回结束。

    if (k == n) {
        solve.push_back(queens);
        return;  
    }

2.然后我们每一个递归函数的意义是枚举每一行,所以我们只需要遍历写一层循环就足以。

 for (int i = 0; i < n; i++) 

3.因为是递归,每一次我们要判断位置是否没有被占领为操作条件,因为位置被占据则无法放置皇后。

for (int i = 0; i < n; i++) {
        if (attack[k][i] == 0) {
            //进行放置皇后和递归操作
        }
    }

4.因为我们一次递归枚举一行,所以当我们每完成一次数据传递的时候,我们要将数据恢复到原来的状态,进行下一次枚举,所以如果需要被记录的状态就要被提前保存,最后都被重置。

    for (int i = 0; i < n; i++) {
        if (attack[k][i] == 0) {
            // 备份attack的操作改为传递引用
            vector<vector<int>> temp = attack;
            // 放置皇后
            queens[k][i] = 'Q';
            // 标记覆盖
            takeOver(k, i, attack, n);
            // 传到下一层
            backtarck(attack, queens, solve, k + 1, n);  //因为c++是这些都是同步操作,而且我们每一次都重置了每一行的更改,所以不会影响接下来的数据
            // 恢复状态
            attack = temp;
            //恢复皇后
		   queens[k][i] = '.';
        }
    }
}

5.在传递到下一次递归函数之前,先把此次枚举的情况标记再传到下一行枚举:

放置皇后:

queens [k] [i] = 'Q';

标记皇后放置时候覆盖的位置: takeOver(k, i, attack, n); //k为行,i为列记录的是皇后的放置位置,然后在attacks上做标记。

总代码:

void backtarck(vector<vector<int>> &attack, vector<string> &queens, vector<vector<string>> &solve, int k, int n) {
    if (k == n) {
        solve.push_back(queens);
        return;  
    }
    for (int i = 0; i < n; i++) {
        if (attack[k][i] == 0) {
            // 备份attack的操作改为传递引用
            vector<vector<int>> temp = attack;
            // 放置皇后
            queens[k][i] = 'Q';
            // 标记覆盖
            takeOver(k, i, attack, n);
            // 传到下一层
            backtarck(attack, queens, solve, k + 1, n);
            // 恢复现场
            attack = temp;
            //恢复皇后
		   queens[k][i] = '.';
        }
    }
}

3.完成组件功能的设计

在完成主体的设计之后,我们来完成标记覆盖的函数编写,如刚才解题思路所说,要将皇后八个方向上的位置都延申覆盖,所以我们可以定义dx[],dy[]来记录attack相对于皇后位置的偏移标记为占领。

        //左,左上,中上,右上,右,右下,中下,左下
        int dx[] = {-1, -1, 0, 1, 1, 1, 0, -1};
        int dy[] = {0, 1, 1, 1, 0, -1, -1, -1};
        //标记皇后位置
        attack[x][y] = 1;

因为是在棋盘上无限延申所以循环 for (int i = 0; i < n; i++):

解释一下下面这个步骤,为什么这样设计会能够无限延申遍历八个方向,i可以理解为相对偏移次数,就比如:

假如我选择的方向是往皇后的右上角走,当i=1的时候,我向皇后的右上角走一格,然后在那格子做标记;当i=2的时候,我向皇后的右上角走一格之后再在此的基础上又往右上角走了一格。

            int xOver = x + i * dx[j];
            int yOver = y + i * dy[j];

最后通过判断边界来确定不越界的情况下(x,y分别不小于0,不超过最大的边界值),对占据的位置做标记:

        if (xOver < n && yOver < n && xOver >= 0 && yOver >= 0) {
                attack[xOver][yOver] = 1;
            }

总代码:

    //标记覆盖
    void takeOver(int x, int y, vector<vector<int>> &attack, int n) {  
        //左,左上,中上,右上,右,右下,中下,左下
        int dx[] = {-1, -1, 0, 1, 1, 1, 0, -1};
        int dy[] = {0, 1, 1, 1, 0, -1, -1, -1};
        //标记皇后位置
        attack[x][y] = 1;
        for (int i = 0; i < n; i++) {
            //八个方向
            for (int j = 0; j < 8; j++) {
                int xOver = x + i * dx[j];
                int yOver = y + i * dy[j];
                //判断边界
                if (xOver < n && yOver < n && xOver >= 0 && yOver >= 0) {
                    attack[xOver][yOver] = 1;
                }
            }
        }
    }

4.完整代码

#include<bits/stdc++.h>
using namespace std;
class Solution {
public:
    //标记覆盖
    void takeOver(int x, int y, vector<vector<int>> &attack, int n) {  
        //左,左上,中上,右上,右,右下,中下,左下
        int dx[] = {-1, -1, 0, 1, 1, 1, 0, -1};
        int dy[] = {0, 1, 1, 1, 0, -1, -1, -1};
        //标记皇后位置
        attack[x][y] = 1;
        for (int i = 0; i < n; i++) {
            //八个方向
            for (int j = 0; j < 8; j++) {
                int xOver = x + i * dx[j];
                int yOver = y + i * dy[j];
                //判断边界
                if (xOver < n && yOver < n && xOver >= 0 && yOver >= 0) {
                    attack[xOver][yOver] = 1;
                }
            }
        }
    }
void backtarck(vector<vector<int>> &attack, vector<string> &queens, vector<vector<string>> &solve, int k, int n) {
    if (k == n) {
        solve.push_back(queens);
        return;
    }
    for (int i = 0; i < n; i++) {
        if (attack[k][i] == 0) {
            // 备份attack的操作改为传递引用
            vector<vector<int>> temp = attack;
            // 放置皇后
            queens[k][i] = 'Q';
            // 标记覆盖
            takeOver(k, i, attack, n);
            // 传到下一层
            backtarck(attack, queens, solve, k + 1, n);
            // 恢复现场
            attack = temp;
            //恢复皇后
		   queens[k][i] = '.';
        }
    }
}
    vector<vector<string>> solveNQueens(int n) {
        int k = 0;
        vector<vector<string>> solve;
        vector<vector<int>> attack(n, vector<int>(n, 0)); // 分配内存并初始化为零
        vector<string> queens(n, string(n,'.')); // 分配内存并初始化为点号
        backtarck(attack, queens, solve, k, n);
        return solve;
    }
};
int main() {
    Solution solution;
    int n = 8; // 测试N皇后问题的规模
    vector<vector<string>> result = solution.solveNQueens(n);

    cout << "Solutions for " << n << " queens problem:" << endl;
    cout << "多少种解 " << result.size()<< " queens problem:" << endl;
    for (const auto& solution : result) {
        for (const string& row : solution) {
        cout << row << std::endl;
        }
        cout << std::endl;
    }

    return 0;
}

参考:

算法与数据结构,回溯法求解八皇后,最经典的递归问题_哔哩哔哩_bilibili

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值