使用位運算優化 N 皇后問題

使用位運算優化 N 皇后問題

作者:Grey

原文地址:

博客園:使用位運算優化 N 皇后問題

CSDN:使用位運算優化 N 皇后問題

問題描述

N 皇后問題是指在 n * n 的棋盤上要擺 n 個皇后,
要求:任何兩個皇后不同行,不同列也不在同一條斜線上,
求給一個整數 n ,返回 n 皇后的擺法數。

題目鏈接:牛客-N皇后問題

常規解法

由於皇后不能共行,所以使用一個一維數組就可以表示整個過程

int[] records = new int[n];

其中 records[i] = j 表示:皇后安排在 i 行的 j 號位置。

在遍歷過程中,可以首先給第 0 行安排一個皇后,然後到下一行安排一個皇后,一直安排到最後一行,如果可以順利走到最後一行,則記錄一種有效的擺法。

整體流程代碼如下

    public static int num1(int n) {
        if (n < 1 || n == 2 || n == 3) {
            return 0;
        }
        if (n == 1) {
            return 1;
        }
        int[] records = new int[n];
        return process1(0, records, n);
    }

    public static int process1(int i, int[] records, int n) {
        if (i == n) {
            return 1;
        }
        int ways = 0;
        for (int j = 0; j < n; j++) {
            if (isValid(records, i, j)) {
                records[i] = j;
                ways += process1(i + 1, records, n);
            }
        }
        return ways;
    }

對於上述代碼進行說明,首先,我們可以過濾掉一些基本的場景,比如

n < 1 || n == 2 || n == 3

這種情況下,怎麼擺都不可能滿足條件。

對於n == 1的情況,只能有一種擺法。

接下來就是int process1(int i, int[] records, int n)這個遞歸函數,這個遞歸函數的遞歸含義是:在棋盤中,0 ~ i - 1 行都已經安排好皇后了,要開始安排第 i 行的皇后了,安排完 i 行的皇后以後,繼續安排到最後,可以得到的有效填充方案有多少?

base case 爲

i == n

即:已經安排完最後一行(i == n - 1)的皇后了,說明之前的決策沒問題,可以得到了有效的一種方案,返回 1。

接下來是普遍情況


        int ways = 0;
        for (int j = 0; j < n; j++) {
            if (isValid(records, i, j)) {
                records[i] = j;
                ways += process1(i + 1, records, n);
            }
        }

枚舉第 i 行每個位置填皇后的情況,然後去下一行繼續安排皇后,但是有個前提,給 i 行的第 j 號位置分配皇后的時候,需要首先校驗下 i 行能否填 j 號皇后,即 isValid() 方法要解決的問題。

boolean isValid(int[] records, int i, int j);

這個方法表示: 0 ~ i - 1 行都安排好皇后的情況下,第 i 行的 j 號位置放皇后,是否合法。

這就涉及到一個簡單的問題:已知二維矩陣中 [x,y][甲,乙] 兩個點,如何判斷其位置關係是否合法?

不合法的情況有兩個,滿足下述任何一種情況,兩個點位置關係就不合法。

情況一:共列的情況,即 y == 乙

image

情況二: 共對角線的情況,即 (甲-x) 的絕對值等於 (乙-y) 的絕對值相等,即 |甲 - x| == |乙 - y|

image

由於我們每行只安排一個皇后,所以不需要判斷兩個點是否共行,在我們的算法模型下,這兩個點天然不共行。

完整代碼如下

public static boolean isValid(int[] records, int i, int j) {
    for (int s = 0; s < i; s++) {
        if (records[s] == j || Math.abs(records[s] - j) == Math.abs(i - s)) {
            return false;
        }
    }
    return true;
}

這個解法的時間時間複雜度是 O(N^N)

位運算優化解

以上是 N 皇后的常規解法,接下來是使用位運算來優化 N 皇后算法。

注:位運算只是減少了常數項的時間,整體時間複雜度還是 O(N^N),且位運算優化解目前支持處理 32 皇后問題。

可以通過以下例子熟悉一下位運算的用法,比如,打印一個 32 位整數的二進制形式(不用 Java 現成的 API),應該如何實現?

    // 打印一個32位整數的二進制形式
    public static void printBinary(int num) {
        for (int i = 31; i >= 0; i--) {
            System.out.print((num & (1 << i)) == 0 ? "0" : "1");
        }
        System.out.println();
    }

思路就是用這個整數的二進制的每一位和對應位置上的是 1 ,其餘位置是 0 的數進行與(&)運算,如果與完以後結果是 0 ,則該位一定是 0 ,否則該位是 1。

再來一個示例,如何獲取一個數二進制最右側的 1?

比如:

7 這個變量,二進制爲 00000000000000000000000000000111,最右側的 1 就是 00000000000000000000000000000001,所以 7 最右側的 1 的值是 1;

22 這個變量,二進制爲 00000000000000000000000000010110,最右側的 1 就是 00000000000000000000000000000010,所以 22 最右側的 1 的值是 2。

結論是,一個數 num,其最右側的 1 的值是 num & (~num + 1) 或者 num & (-num)

有了上述鋪墊,

接下來是 N 皇后問題的優化點,在常規方法中,使用 records[] 數組來記錄皇后的位置信息,如果用位運算優化解,可以使用一個 32 位整型變量的二進制狀態信息來存儲皇后的位置信息,

比如,常規解法中 records[x] == 5,表示某一行的 5 號位置有一個皇后,如果用 32 位狀態信息來表示,則爲

image

以上信息用一個變量pos來表示,這個變量就用於記錄哪個列位置中填了皇后,

還要設置另外三個變量

// 皇后的列限制是什麼
int colLim;
// 皇后的左下對角線限制是什麼
int leftDiaLim;
// 皇后的右下對角線限制是什麼
int rightDiaLim;

比如,5 皇后問題,初始狀態下,pos == 0 ,然後在第 4 個位置填了一個皇后,即 pos 二進制的第 4 個位置變爲 1,那麼其對應的三個變量的變化如下。

image

接下來定義一個變量

int limit = n == 32 ? -1 : (1 << n) - 1;

由於用的是 32 位整型變量,所以最大支持 32 皇后問題,

如果是小於 32 的皇后問題,比如 13 皇后問題,那麼 limit 會使用其二進制的最右側的 13 個位置,會將最右側的 13 個位置設置爲 1,即 1 << n - 1

如果正好是 32 皇后問題,則爲 -1,即二進制的 32 個位置都是 1。

有了colLim,leftDiaLim,rightDiaLim,limit這四個變量,就可以決策出下一個可以擺放皇后的位置,

int pos = limit & (~(colLim | leftDiaLim | rightDiaLim)) 得到的結果中,pos的二進制狀態上是 1 的,就是可以放皇后的位置。

如果colLim == limit,說明每一列都安排好了皇后,直接返回一種有效解法。

得到 pos 變量後,從右往左依次枚舉其二進制上爲 1 的位置,在該位置放上皇后,把該位置列限制(即:colLim變量),左對角線限制(即:leftDiaLim變量),右對角線限制(即:rightDiaLim變量)進行對應的調整,然後跑後續的遞歸過程收集所有的有效佈局次數。

完整代碼如下

    //  請不要超過32皇后問題
    public static int num2(int n) {
        if (n < 1 || n > 32) {
            return 0;
        }
        // 如果你是13皇后問題,limit 最右13個1,其他都是0
        int limit = n == 32 ? -1 : (1 << n) - 1;
        return process2(limit, 0, 0, 0);
    }

    // 7皇后問題
    // limit : 0....0 1 1 1 1 1 1 1
    // 之前皇后的列影響:colLim
    // 之前皇后的左下對角線影響:leftDiaLim
    // 之前皇后的右下對角線影響:rightDiaLim
    public static 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;
        int res = 0;
        while (pos != 0) {
            // 得到 pos 最右側的 1
            mostRightOne = pos & (~pos + 1);
            // 在該位置放上皇后,即:把該位置設置爲 0
            pos = pos - mostRightOne;
            res += process2(limit, colLim | mostRightOne, (leftDiaLim | mostRightOne) << 1, (rightDiaLim | mostRightOne) >>> 1);
        }
        return res;
    }

更多

算法和數據結構筆記

參考資料

算法和數據結構體系班-左程雲

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章