N皇后問題

n1ce2cv發表於2024-10-06

N皇后問題

  • 時間複雜度為 O(n!)

51. N 皇后

經典做法

#include <string>
#include <iostream>
#include <vector>
#include <unordered_set>

using namespace std;

class Solution {
public:
    vector<vector<string>> res;
    // 分別標記列和兩個方向的斜線上是否已經存在皇后
    unordered_set<int> columns;
    unordered_set<int> diagonals1;
    unordered_set<int> diagonals2;

    vector<vector<string>> solveNQueens(int n) {
        // 記錄每一行上,皇后所在的列
        vector<int> queens(n, -1);
        backtrack(queens, n, 0);
        return res;
    }

    void backtrack(vector<int> &queens, int n, int row) {
        if (row == n) {
            // 結算這種情況
            vector<string> board = generateBoard(queens, n);
            res.push_back(board);
            return;
        }
        // 嘗試在每一列上放
        for (int i = 0; i < n; i++) {
            // 當前列已有皇后
            if (columns.find(i) != columns.end()) continue;
            // 主斜線已有
            int d1 = row - i;
            if (diagonals1.find(d1) != diagonals1.end()) continue;
            // 副斜線已有
            int d2 = row + i;
            if (diagonals2.find(d2) != diagonals2.end()) continue;

            // 把 row 行的皇后放在 i 列
            queens[row] = i;
            // 標記列和兩個方向的斜線上已經存在皇后
            columns.insert(i);
            diagonals1.insert(d1);
            diagonals2.insert(d2);
            // 遞迴處理下一行
            backtrack(queens, n, row + 1);
            // 取消標記
            queens[row] = -1;
            columns.erase(i);
            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 = string(n, '.');
            row[queens[i]] = 'Q';
            board.push_back(row);
        }
        return board;
    }
};

位運算

#include <string>
#include <vector>

using namespace std;

class Solution {
public:
    vector<vector<string>> res;
    // 記錄皇后放的位置,queens[i] 二進位制位為 1 的地方才是放皇后的位置
    // 也可以直接記錄具體列號,這樣生成結果時快些
    vector<int> queens;
    int limit;

    vector<vector<string>> solveNQueens(int n) {
        // 把低 n 位變成 1
        limit = (1 << n) - 1;
        // -1 的位置表示沒有皇后
        queens.resize(n, -1);
        backtrack(n, 0, 0, 0, 0);
        return res;
    }

    void backtrack(int n, int row, int columns, int diagonals1, int diagonals2) {
        if (columns == limit) {
            // 生成結果
            res.emplace_back(generateBoard(n));
            return;
        }
        // 0 的位置能放,1 的位置不能放
        int ban = columns | diagonals1 | diagonals2;
        // candidate 為 1 的地方都是可以放皇后的
        int candidate = limit & (~ban);
        // 嘗試每個位置
        while (candidate != 0) {
            // 最右側的 1
            int place = candidate & (-candidate);
            queens[row] = place;
            // 累計上當前皇后的影響,(diagonals1 | place) >> 1 的意思是當前 place 位置放皇后的情況下,主斜線對下一行的影響
            backtrack(n, row + 1, columns | place, (diagonals1 | place) >> 1, (diagonals2 | place) << 1);
            // 刪掉最右側的 1
            candidate ^= place;
        }
    }

    vector<string> generateBoard(int n) {
        vector<string> board;
        for (int i = 0; i < n; i++) {
            string str;
            for (int j = 0; j < n; ++j) {
                if ((queens[i] & (1 << j)) != 0) {
                    str += 'Q';
                } else {
                    str += '.';
                }
            }
            board.emplace_back(str);
        }
        return board;
    }
};

52. N 皇后 II

經典做法

  • 常數時間慢

  • 過程

    • 用陣列記錄每一行的皇后所在列
    • 到 row 行時,根據之前行上的皇后位置,判斷能放在哪些列
    • 把所有能放的列都嘗試一邊,每次嘗試修改路徑陣列表示當前的決策
  1. 用 set 標記列和斜線上是否已經存在皇后
#include <string>
#include <iostream>
#include <vector>
#include <unordered_set>

using namespace std;

class Solution {
public:
    // 分別標記列和兩個方向的斜線上是否已經存在皇后
    unordered_set<int> columns;
    unordered_set<int> diagonals1;
    unordered_set<int> diagonals2;
    // 記錄每行的皇后在哪一列
    vector<int> queens;
    int res;

    void backtrack(int n, int row) {
        if (row == n) {
            res++;
            return;
        }
        for (int i = 0; i < n; ++i) {
            // 如果不能放就跳過
            if (columns.find(i) != columns.end()) continue;
            int d1 = i - row;
            if (diagonals1.find(d1) != diagonals1.end()) continue;
            int d2 = i + row;
            if (diagonals2.find(d2) != diagonals2.end()) continue;

            queens[row] = i;
            // 標記
            columns.emplace(i);
            diagonals1.emplace(d1);
            diagonals2.emplace(d2);
            // 遞迴處理子問題
            backtrack(n, row + 1);
            // 回溯
            queens[row] = -1;
            columns.erase(i);
            diagonals1.erase(d1);
            diagonals2.erase(d2);
        }

    }

    int totalNQueens(int n) {
        res = 0;
        // -1 表示沒有放皇后
        queens.resize(n, -1);
        backtrack(n, 0);
        return res;
    }
};
  1. 遍歷之前的皇后,檢查和當前位置是否衝突
#include <string>
#include <iostream>
#include <vector>
#include <unordered_set>

using namespace std;

class Solution {
public:
    // 記錄每行的皇后在哪一列
    vector<int> queens;
    int res;

    void backtrack(int n, int row) {
        if (row == n) {
            res++;
            return;
        }
        for (int column = 0; column < n; ++column) {
            if (!isValid(row, column)) continue;
            queens[row] = column;
            // 遞迴處理子問題
            backtrack(n, row + 1);
            // 下個嘗試的列會覆蓋掉 queens[row],所以不需要手動回溯
        }
    }

    // 判斷能否在當前位置放皇后
    bool isValid(int row, int column) {
        for (int i = 0; i < row; ++i)
            if (column == queens[i] || abs(row - i) == abs(column - queens[i]))
                return false;
        return true;
    }

    int totalNQueens(int n) {
        res = 0;
        // -1 表示沒有放皇后
        queens.resize(n, -1);
        backtrack(n, 0);
        return res;
    }
};
  1. 遞迴帶返回值
#include <string>
#include <iostream>
#include <vector>
#include <unordered_set>

using namespace std;

class Solution {
public:
    // 記錄每行的皇后在哪一列
    vector<int> queens;

    // 返回: 0...row-1 行已經擺完了,row....n - 1 行可以去嘗試的情況下還能找到幾種有效的方法
    int backtrack(int n, int row) {
        if (row == n) return 1;
        int res = 0;
        for (int column = 0; column < n; ++column) {
            if (!isValid(row, column)) continue;
            queens[row] = column;
            // 遞迴處理子問題
            res += backtrack(n, row + 1);
        }
        return res;
    }

    // 判斷能否在當前位置放皇后
    bool isValid(int row, int column) {
        for (int i = 0; i < row; ++i)
            if (column == queens[i] || abs(row - i) == abs(column - queens[i]))
                return false;
        return true;
    }

    int totalNQueens(int n) {
        queens.resize(n, -1);
        return backtrack(n, 0);
    }
};

位運算

  • 常數時間快
using namespace std;

class Solution {
public:
    int res;
    int limit;

    // columns 按位標記哪些列上已經有皇后了
    // diagonals1、diagonals2 標記兩種方向的斜線上對當前行的影響,為 1 的位置表示在這個斜線上已經有過皇后了
    void backtrack(int columns, int diagonals1, int diagonals2) {
        if (columns == limit) {
            // 低 n 位全是 1,說明每一列都放皇后了,結束
            res++;
            return;
        }

        // 0 的位置能放,1 的位置不能放
        int ban = columns | diagonals1 | diagonals2;
        // candidate 為 1 的地方都是可以放皇后的
        int candidate = limit & (~ban);
        // 嘗試每個位置
        while (candidate != 0) {
            // 放皇后的具體位置:取出 candidate 最右側的 1,也就是列從左往右數第一個能放皇后的列
            int place = candidate & (-candidate);
            // 累計上當前皇后的影響,(diagonals1 | place) >> 1 的意思是當前 place 位置放皇后的情況下,主斜線對下一行的影響
            backtrack(columns | place, (diagonals1 | place) >> 1, (diagonals2 | place) << 1);
            // 把 candidate 最右側的 1 變成 0,然後嘗試下個能放皇后的位置
            candidate ^= place;
        }
    }

    int totalNQueens(int n) {
        res = 0;
        // 把低 n 位變成 1
        limit = (1 << n) - 1;
        backtrack(0, 0, 0);
        return res;
    }
};

相關文章