使用位運算優化 N 皇后問題
作者:Grey
原文地址:
問題描述
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 == 乙
情況二: 共對角線的情況,即 (甲-x)
的絕對值等於 (乙-y)
的絕對值相等,即 |甲 - x| == |乙 - y|
。
由於我們每行只安排一個皇后,所以不需要判斷兩個點是否共行,在我們的算法模型下,這兩個點天然不共行。
完整代碼如下
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 位狀態信息來表示,則爲
以上信息用一個變量pos
來表示,這個變量就用於記錄哪個列位置中填了皇后,
還要設置另外三個變量
// 皇后的列限制是什麼
int colLim;
// 皇后的左下對角線限制是什麼
int leftDiaLim;
// 皇后的右下對角線限制是什麼
int rightDiaLim;
比如,5 皇后問題,初始狀態下,pos == 0
,然後在第 4 個位置填了一個皇后,即 pos
二進制的第 4 個位置變爲 1,那麼其對應的三個變量的變化如下。
接下來定義一個變量
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;
}