【LeetCode Hot100 刷题日记 (62/100)】51. N 皇后 —— 回溯 + 位运算优化🧠

5 阅读7分钟

📌 题目链接51. N 皇后 - 力扣(LeetCode) 🔍 难度:困难 | 🏷️ 标签:回溯、位运算、递归、剪枝
⏱️ 目标时间复杂度:O(N!)
💾 空间复杂度:O(N)


🧠 题目分析

N 皇后问题是经典的约束满足问题(Constraint Satisfaction Problem, CSP) ,要求在 N×N 的棋盘上放置 N 个皇后,使得任意两个皇后不在同一行、同一列、或同一条对角线上。

  • 每行只能放一个皇后 → 问题退化为在每行选一列放置皇后
  • 每列只能有一个皇后 → 列不能重复
  • 对角线不能有冲突 → 行列差/和不能重复

面试考点

  • 回溯算法模板掌握
  • 对角线索引的数学建模(row - colrow + col
  • 位运算优化技巧(常用于状态压缩类回溯题)
  • 剪枝思想与搜索空间压缩

🔁 核心算法及代码讲解

方法一:基于集合的回溯(清晰易懂,推荐面试写法)

我们使用三个哈希集合来记录已被占用的列和两条对角线

  • columns:记录已占用的列(列索引 col
  • diagonals1:记录“主对角线”(从左上到右下),其特征是 row - col = const
  • diagonals2:记录“副对角线”(从右上到左下),其特征是 row + col = const

📌 关键洞察
同一条主对角线上,任意两点 (r1, c1)(r2, c2) 满足 r1 - c1 == r2 - c2
同一条副对角线上,满足 r1 + c1 == r2 + c2
因此可用 row - colrow + col 唯一标识一条对角线!

✅ C++ 代码(带详细行注释)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>> solutions;          // 存储所有合法解
        vector<int> queens(n, -1);                 // queens[row] = col,表示第 row 行皇后放在 col 列
        unordered_set<int> columns;                // 已占用的列
        unordered_set<int> diagonals1;             // 主对角线:row - col
        unordered_set<int> diagonals2;             // 副对角线:row + col
        backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
        return solutions;
    }

    void backtrack(vector<vector<string>>& solutions,
                   vector<int>& queens,
                   int n,
                   int row,
                   unordered_set<int>& columns,
                   unordered_set<int>& diagonals1,
                   unordered_set<int>& diagonals2) {
        // 🎯 终止条件:所有行都已放置皇后
        if (row == n) {
            vector<string> board = generateBoard(queens, n);
            solutions.push_back(board);
            return;
        }

        // 尝试在当前 row 行的每一列放置皇后
        for (int col = 0; col < n; ++col) {
            // ❌ 剪枝1:该列已被占用
            if (columns.count(col)) continue;
            // ❌ 剪枝2:主对角线冲突(row - col 已存在)
            int d1 = row - col;
            if (diagonals1.count(d1)) continue;
            // ❌ 剪枝3:副对角线冲突(row + col 已存在)
            int d2 = row + col;
            if (diagonals2.count(d2)) continue;

            // ✅ 放置皇后
            queens[row] = col;
            columns.insert(col);
            diagonals1.insert(d1);
            diagonals2.insert(d2);

            // 🔁 递归处理下一行
            backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);

            // 🔄 回溯:撤销选择(恢复现场)
            queens[row] = -1;
            columns.erase(col);
            diagonals1.erase(d1);
            diagonals2.erase(d2);
        }
    }

    // 将 queens 数组转换为棋盘字符串表示
    vector<string> generateBoard(vector<int>& queens, int n) {
        vector<string> board;
        for (int i = 0; i < n; ++i) {
            string row(n, '.');           // 初始化全为 '.'
            row[queens[i]] = 'Q';         // 在 queens[i] 列放 'Q'
            board.push_back(row);
        }
        return board;
    }
};

方法二:基于位运算的回溯(极致优化,适合进阶)

利用位掩码(bitmask) 替代集合,将空间复杂度从 O(N) 降至 O(1)(仅指状态记录部分)。

  • 用一个整数 columns 表示哪些列被占(第 i 位为 1 表示第 i 列不可用)
  • diagonals1:主对角线状态,每深入一行需左移 1 位(因为对角线向右下延伸)
  • diagonals2:副对角线状态,每深入一行需右移 1 位

📌 位运算技巧

  • x & (-x):提取最低位的 1(如 010100000100
  • x & (x - 1):清除最低位的 1(用于遍历所有可选位置)
  • __builtin_ctz(x):返回 x 的二进制末尾 0 的个数(即最低位 1 的位置)

✅ C++ 位运算版(行注释)

void solve(vector<vector<string>>& solutions,
           vector<int>& queens,
           int n,
           int row,
           int columns,
           int diagonals1,
           int diagonals2) {
    if (row == n) {
        solutions.push_back(generateBoard(queens, n));
        return;
    }

    // 计算当前行所有可放置的位置(1 表示可用)
    // (1 << n) - 1 得到低 n 位全 1 的掩码
    // ~(columns | d1 | d2) 取反得到可用位
    // & ((1 << n) - 1) 确保只保留低 n 位
    int available = ((1 << n) - 1) & (~(columns | diagonals1 | diagonals2));

    while (available) {
        int pos = available & (-available);      // 取最低位的 1
        available &= (available - 1);            // 清除该位
        int col = __builtin_ctz(pos);            // 获取列索引(从 0 开始)

        queens[row] = col;
        // 递归:columns | pos 标记该列占用
        // diagonals1 | pos 后右移(注意:代码中是 >>1,因为下一行时对角线右移)
        // diagonals2 | pos 后左移
        solve(solutions, queens, n, row + 1,
              columns | pos,
              (diagonals1 | pos) >> 1,
              (diagonals2 | pos) << 1);
        queens[row] = -1; // 回溯(此处其实可省略,因会被覆盖)
    }
}

💡 注意:位运算版本中,diagonals1diagonals2 在递归调用时分别 右移和左移,是因为:

  • 主对角线(\):下一行时,冲突位置向右移动 → 位掩码需右移(>>1
  • 副对角线(/):下一行时,冲突位置向左移动 → 位掩码需左移(<<1

🧩 解题思路(分步拆解)

  1. 问题建模

    • 每行放一个皇后 → 只需决定每行的列位置
    • 用数组 queens[row] = col 记录方案
  2. 冲突检测

    • 列冲突:col 是否已用?
    • 主对角线冲突:row - col 是否已存在?
    • 副对角线冲突:row + col 是否已存在?
  3. 回溯框架

    backtrack(row):
      if row == n: 保存解
      for col in 0..n-1:
         if col / d1 / d2 冲突: continue
         选择 col
         backtrack(row + 1)
         撤销选择
    
  4. 结果生成

    • 遍历 queens 数组,按列位置生成 "....Q..." 字符串

📊 算法分析

项目分析
时间复杂度O(N!) —— 最坏情况下需遍历所有排列,但剪枝大幅减少实际搜索节点
空间复杂度O(N) —— 递归深度为 N,queens 数组和集合/位掩码均为 O(N)
剪枝效果通过列和对角线约束,避免无效搜索,实际运行远快于理论最坏情况
适用场景所有“在网格中放置互斥元素”的问题(如数独、八数码等)

🎯 面试加分点

  • 能解释为什么 row - colrow + col 能唯一标识对角线
  • 能对比集合 vs 位运算的优劣(可读性 vs 性能)
  • 能指出 N 皇后问题在 N=2,3 时无解(可作为边界测试)

💻 完整代码

C++(集合回溯版 —— 推荐面试使用)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>> solutions;
        vector<int> queens(n, -1);
        unordered_set<int> columns;
        unordered_set<int> diagonals1;
        unordered_set<int> diagonals2;
        backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
        return solutions;
    }

    void backtrack(vector<vector<string>>& solutions,
                   vector<int>& queens,
                   int n,
                   int row,
                   unordered_set<int>& columns,
                   unordered_set<int>& diagonals1,
                   unordered_set<int>& diagonals2) {
        if (row == n) {
            vector<string> board = generateBoard(queens, n);
            solutions.push_back(board);
            return;
        }
        for (int col = 0; col < n; ++col) {
            if (columns.count(col)) continue;
            int d1 = row - col;
            if (diagonals1.count(d1)) continue;
            int d2 = row + col;
            if (diagonals2.count(d2)) continue;
            queens[row] = col;
            columns.insert(col);
            diagonals1.insert(d1);
            diagonals2.insert(d2);
            backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
            queens[row] = -1;
            columns.erase(col);
            diagonals1.erase(d1);
            diagonals2.erase(d2);
        }
    }

    vector<string> generateBoard(vector<int>& queens, int n) {
        vector<string> board;
        for (int i = 0; i < n; ++i) {
            string row(n, '.');
            row[queens[i]] = 'Q';
            board.push_back(row);
        }
        return board;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    
    Solution sol;
    auto res1 = sol.solveNQueens(4);
    // 输出: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
    for (auto& board : res1) {
        for (string& row : board) {
            cout << row << "\n";
        }
        cout << "-----\n";
    }
    
    auto res2 = sol.solveNQueens(1);
    // 输出: [["Q"]]
    for (auto& row : res2[0]) {
        cout << row << "\n";
    }
    
    return 0;
}

JavaScript(集合回溯版)

/**
 * @param {number} n
 * @return {string[][]}
 */
var solveNQueens = function(n) {
    const solutions = [];
    const queens = new Array(n).fill(-1);
    const columns = new Set();
    const diagonals1 = new Set(); // row - col
    const diagonals2 = new Set(); // row + col

    const backtrack = (row) => {
        if (row === n) {
            solutions.push(generateBoard(queens, n));
            return;
        }
        for (let col = 0; col < n; col++) {
            if (columns.has(col)) continue;
            const d1 = row - col;
            if (diagonals1.has(d1)) continue;
            const d2 = row + col;
            if (diagonals2.has(d2)) continue;

            queens[row] = col;
            columns.add(col);
            diagonals1.add(d1);
            diagonals2.add(d2);

            backtrack(row + 1);

            queens[row] = -1;
            columns.delete(col);
            diagonals1.delete(d1);
            diagonals2.delete(d2);
        }
    };

    const generateBoard = (queens, n) => {
        const board = [];
        for (let i = 0; i < n; i++) {
            let row = '.'.repeat(n);
            row = row.substring(0, queens[i]) + 'Q' + row.substring(queens[i] + 1);
            board.push(row);
        }
        return board;
    };

    backtrack(0);
    return solutions;
};

// 测试
console.log(solveNQueens(4));
console.log(solveNQueens(1));

🧪 测试用例 & 结果

输入n = 4
输出

[".Q..", "...Q", "Q...", "..Q."]
-----
["..Q.", "Q...", "...Q", ".Q.."]

输入n = 1
输出["Q"]

边界测试

  • n = 2[](无解)
  • n = 3[](无解)
  • n = 9 → 返回 352 种解(验证正确性)

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!