【回溯算法巅峰之作】LeetCode 51. N皇后问题详解与常见避坑指南 (C/C++/Python)

今天我们来死磕一道回溯算法里大名鼎鼎的"巅峰之作"------N皇后问题

很多人一听到"N皇后"就觉得高深莫测,但如果你跟着"代码随想录"的思路,把树形结构画出来,就会发现它其实就是一个标准的回溯模板题。

今天这篇文章,我们将从核心逻辑拆解,到 isValid 函数的细节剖析,再到多语言(C, C++, Python)的代码实现,最后附带新手高频踩坑点总结,帮你彻底拿下这道题!


一、 核心思路:回溯三部曲

N皇后问题的本质是:在一个N * N 的棋盘上放置 N 个皇后,使得她们不能互相攻击(不能同行、同列、同对角线)。

我们采取的策略是按行放置:从第 0 行开始,逐行往下放。这样我们就天然地避开了"同行"的冲突。

在树形结构中:

  • 树的深度(递归) :代表棋盘的row)。

  • 树的宽度(for循环) :代表棋盘的col)。

1. 递归函数参数

我们需要传入当前棋盘的大小 n,当前处理到的行数 row,以及当前的棋盘状态 chessboard

2. 终止条件

当我们走到叶子节点,也就是 row == n 时,说明 N 个皇后都已经成功放置完毕。此时将当前棋盘状态加入结果集 res,然后 return

3. 单层搜索逻辑

在当前行 row,我们用一个 for 循环遍历所有的列 col

如果当前位置 (row, col) 可以放皇后(通过 isValid 验证):

  1. 放置皇后:chessboard[row][col] = 'Q'

  2. 递归进入下一行:backtracking(row + 1)

  3. 回溯(撤销操作)chessboard[row][col] = '.'

偷一下卡哥的图


二、 灵魂拷问:isValid 验证函数怎么写?

在放置皇后之前,必须检查当前位置是否安全。因为我们是从上往下,一行一行 放的,当前行 row 的下方全是空的。

所以,我们只需要检查三个方向(也就是向上看的三个方向):

  1. 正上方:同一列是否有皇后?

  2. 左上对角线:45度角是否有皇后?

  3. 右上对角线:135度角是否有皇后?

(注:不需要检查同一行,因为 for 循环每次只会在当前行选一个位置放;也不需要检查下方,因为还没走到那里。)


三、 多语言代码实现

1. Python 3 实现(附带切片技巧)

Python 中字符串是不可变的,所以修改棋盘某一个字符时,需要用到字符串切片拼接。

python 复制代码
from typing import List

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        res = []
        # 初始化空棋盘
        qipan = ["." * n for _ in range(n)]

        def backtracking(row):
            # 终止条件:走完最后一行
            if row == n:
                res.append(qipan[:])
                return

            # 遍历当前行的每一列
            for col in range(n):
                if isValid(row, col):
                    # 放置皇后 (注意:修改的是当前行 qipan[row])
                    qipan[row] = qipan[row][:col] + "Q" + qipan[row][col + 1:]
                    backtracking(row + 1)
                    # 回溯:撤销皇后
                    qipan[row] = qipan[row][:col] + "." + qipan[row][col + 1:]

        def isValid(row, col):
            # 1. 检查正上方
            for i in range(row):
                if qipan[i][col] == "Q": return False
            
            # 2. 检查左上方对角线
            i, j = row - 1, col - 1
            while i >= 0 and j >= 0:
                if qipan[i][j] == "Q": return False
                i -= 1; j -= 1
                
            # 3. 检查右上方对角线
            i, j = row - 1, col + 1
            while i >= 0 and j < n:
                if qipan[i][j] == "Q": return False
                i -= 1; j += 1
                
            # 三个方向都安全,返回 True
            return True

        backtracking(0)
        return res

2. C++ 实现 (代码随想录经典版)

C++ 的 std::string 是可变的,直接用索引修改即可,代码非常清爽。

cpp 复制代码
#include <vector>
#include <string>

using namespace std;

class Solution {
private:
    vector<vector<string>> result;

    void backtracking(int n, int row, vector<string>& chessboard) {
        if (row == n) {
            result.push_back(chessboard);
            return;
        }

        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] = '.'; // 回溯,撤销皇后
            }
        }
    }

    bool isValid(int row, int col, vector<string>& chessboard, int n) {
        // 检查列
        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;
    }

public:
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        vector<string> chessboard(n, string(n, '.'));
        backtracking(n, 0, chessboard);
        return result;
    }
};

3. C 语言实现

C 语言处理二维字符串数组稍微繁琐一些,需要手动开辟和释放内存,但核心思想完全一致。

objectivec 复制代码
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

bool isValid(char** chessboard, int row, int col, int n) {
    // 检查正上方
    for (int i = 0; i < row; i++) {
        if (chessboard[i][col] == 'Q') return false;
    }
    // 检查左上方
    for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
        if (chessboard[i][j] == 'Q') return false;
    }
    // 检查右上方
    for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
        if (chessboard[i][j] == 'Q') return false;
    }
    return true;
}

void backtracking(int n, int row, char** chessboard, char**** result, int* returnSize) {
    if (row == n) {
        char** temp = (char**)malloc(n * sizeof(char*));
        for (int i = 0; i < n; i++) {
            temp[i] = (char*)malloc((n + 1) * sizeof(char));
            strcpy(temp[i], chessboard[i]);
        }
        (*result)[*returnSize] = temp;
        (*returnSize)++;
        return;
    }

    for (int col = 0; col < n; col++) {
        if (isValid(chessboard, row, col, n)) {
            chessboard[row][col] = 'Q';
            backtracking(n, row + 1, chessboard, result, returnSize);
            chessboard[row][col] = '.'; // 回溯
        }
    }
}

char*** solveNQueens(int n, int* returnSize, int** returnColumnSizes) {
    char*** result = (char***)malloc(1000 * sizeof(char**));
    *returnSize = 0;

    char** chessboard = (char**)malloc(n * sizeof(char*));
    for (int i = 0; i < n; i++) {
        chessboard[i] = (char*)malloc((n + 1) * sizeof(char));
        for (int j = 0; j < n; j++) chessboard[i][j] = '.';
        chessboard[i][n] = '\0';
    }

    backtracking(n, 0, chessboard, &result, returnSize);

    *returnColumnSizes = (int*)malloc(*returnSize * sizeof(int));
    for (int i = 0; i < *returnSize; i++) {
        (*returnColumnSizes)[i] = n;
    }

    // 释放初始化的 chessboard
    for(int i=0; i<n; i++) free(chessboard[i]);
    free(chessboard);

    return result;
}

四、 总结与高频避坑指南

在实际敲代码的过程中,逻辑懂了不代表能一遍过(作者本人就踩过坑!)。以下是大家最容易犯的几个错误,请务必避雷:

坑点 1:Python 中的变量赋值越界

在 Python 中,棋盘 qipan 是一个字符串列表(List[str])。

  • 错误写法qipan = qipan[row][:col] + "Q" + qipan[row][col+1:]

    • 后果 :这会把原本包含 N 个字符串的 qipan 列表,直接覆盖 成了一个单独的字符串!导致递归进入下一层时,发生严重的 IndexError 索引越界。
  • 正确写法qipan[row] = qipan[row][:col] + "Q" + qipan[row][col+1:]

    • 正解 :我们必须精准定位,只修改 qipan 列表中的第 row 个元素

坑点 2:验证函数忘记返回 True

在写 isValid 时,我们很自然地会把所有的 if (冲突) return False 写完。

  • 后果 :如果函数最后忘记写 return True,Python 会默认返回 None(被当作 False 处理),C++ 则会导致未定义行为。这会导致程序认为整个棋盘都没有合法位置,最终输出空集 []

  • 正解 :永远记住在所有冲突检查的 for/while 循环结束后,加上一个兜底的 return True(意味着历经九九八十一难,终于安全了)。

总结

N皇后问题并没有想象中那么难,只要抓住"按行递归,按列遍历"的核心,并在验证时把控好边界,就能轻松拿下。希望这篇博客对你有所帮助。

照例贴上卡哥的代码随想录

51. N皇后 | 回溯 | N皇后 | 剪枝 | 代码随想录-全网最全算法数据结构刷题学习路线|图文+视频教程|免费开源

相关推荐
UrSpecial2 小时前
基于C语言与Epoll的Reactor模型
c语言·网络编程·reactor·epoll
mftang2 小时前
BSS段、Data段、Text段的具体含义和数据特性
数据库·算法
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【反悔贪心】:建筑抢修
c++·算法·贪心·反悔贪心·csp·信奥赛·建筑抢修
TianFuRuanJian3 小时前
科普 | 仿真中的“体力活”:网格验证能不能自动化?
算法·仿真·ai网格
12.=0.3 小时前
【stm32_6.1】串行异步接口USART,串口的原理和应用
c语言·stm32·单片机·嵌入式硬件
leoufung3 小时前
LeetCode 135. Candy:从直觉到最优解的完整推导
算法·leetcode·职场和发展
啧不应该啊3 小时前
Day1 C与python输入输出语句区别
c语言·开发语言
WHS-_-20223 小时前
Tensor-Based Target Sensing for Resource-Irregular ISAC Systems
linux·人工智能·算法