八皇后問題是一個以國際象棋爲背景的問題:如何可以在8×8個格子的國際象棋棋盤上放置八個皇后,使得任何一個皇后都沒法直接吃掉其餘的皇后, 爲了達到此目的,任兩個皇后都不能處於同一條橫行、縱行或斜線上(中國象棋,車能夠走橫線,縱線),問有多少種擺法,高斯認爲有76種方案。1854年在柏林的象棋雜誌上不一樣的做者發表了40種不一樣的解,後來有人用圖論的方法解出92種結果。計算機發明後,有多種計算機語言能夠編程解決此問題。html
八皇后問題能夠推廣爲更通常的n皇后擺放問題:這時棋盤的大小變爲n×n,而皇后個數也變成n。java
當且僅當 n = 1或 n ≥ 4時問題有解.算法
皇后可攻擊範圍如圖所示:圖中藍色區塊不容許放其餘皇后編程
力扣(leetcode)原題連接:https://leetcode-cn.com/problems/n-queens-ii/數組
該題官方提供了2種方式,可是官方講解的並不容易理解,且在主對角線和次對角線描述反了,利用位運算的方法只提供了方法,我經過本身的理解畫了圖形,所謂有圖有真相,方便理解一些,在參考其餘題解後,我再添加了一種利用斜率判斷安全位置的解法。安全
1.窮舉法
時間複雜度:O(n^n)
如八皇后:8^8 = 16777216
進行暴力窮舉: n個for循環嵌套遍歷,找出全部合適的擺放方式,效率低下,不推薦!code
2.遞歸和回溯法
時間複雜度: O(n!) =n*(n-2)(n-4)...
空間複雜度: O(n),保存對角線是否被佔用以及列的信息htm
回溯算法:
解決一個回溯問題,實際上就是一個決策樹的遍歷過程。
一、路徑:也就是已經作出的選擇。
二、選擇列表:也就是你當前能夠作的選擇。
三、結束條件:也就是到達決策樹底層,沒法再作選擇的條件。blog
DFS(深度優先搜索算法) 也採用了回溯源節點方式直到遍歷完全部節點。遞歸
n皇后問題特色:
1.一行一列只能有一個皇后 (橫線,縱線攻擊), 且每行、每列必須有一個皇后 (n*n 棋盤 要擺放n個皇后)
2.皇后佔據的每條斜線有規律,可據此判斷當前位置是否安全(主對角線和次對角線的規律 )
擺放要求:任兩個皇后都不能處於同一條橫行、縱行或斜線上,問有多少種擺法
棋盤可用一個二維數組表示
1.不能在同一行,每一個皇后放置在一行後,保證下一個皇后在下一行擺放。
2.不能在同一列,用一維數組記錄每列是否被皇后佔據,1:被佔用; 0: 未被佔用
3.如何肯定是否在同一條斜線上?
有2種方式
方法一:主對角線和次對角線上的常數規律
相同主對角線上: row -col = 固定常數
並且每條對角線的常數值不一樣,所以用來標識該對角線是否已經被皇后佔用
以下圖所示:第一條主對角線上,row= col , row =col = 0 ,由右上數第二條 爲-1,-2 ... 直到-7。
注:
注: 若是在主對角線右上側, col> row , 所以 const是負數,若是用數組下標索引存儲會致使越界異常,所以可加一個固定正整數(大於等於n)避免 (下見代碼可理解)。
相同次對角線上:row+col = const,每條次對角線的值也不相同。從上到下對角線的值從0到14依次遞增
方法二:根據斜率肯定在同一條斜線上
利用兩點之間斜率絕對值爲 1,即夾角的正切值爲1,夾角爲 45度或者 135度,據此判斷是否在其餘皇后的攻擊斜線上。
公式: |y2-y1| = |x2-x1|
斜率如圖所示:
在放置第三個皇后時,排除已有皇后佔據的列,再剩下位置中與已經放置皇后的座標計算斜率,若是斜率絕對值爲1,即不是安全位置。
動態展現圖:
方法一:官方解法,利用數組記錄被佔據的列信息,以及根據每條斜線被佔據記錄。
public void initQueen(int n) { /** * 一行一列只能有一個queen,記錄某列是否被佔用,int數組記錄,數組下標表示列;1表示佔用,0表示未被佔用 */ int[] cols = new int[n]; /**主對角線特色: row -col = const, 對角線的個數是 2*n-1 * 由於考慮row-col爲負數狀況(右上三角),會發生越界異常,加一個固定常數n,因此這裏數組長度也變爲 3*n-1, * 官方代碼長度延長了2n, 即4*n-1。 */ int[] zhuDiagonal = new int[3*n - 1]; /** * 次對角線特色: row+col =const,次對角線不用延長數組長度,對角線的條數即爲 2*n-1 */ int[] ciDiagonal = new int[ 2*n - 1]; // 調用遞歸回溯方法,下見 int count = back2Track(0, 0, n, cols, zhuDiagonal, ciDiagonal); System.out.printf("共有%d 種方法排列",count); }
public boolean isSafe(int row,int col,int n,int[] cols,int[] zhu, int[] ci) { int res = cols[col] + zhu[row-col+ n] + ci[row+col]; return res ==0 ? true:false; } //若是當前位置列,主對角線,次對線均爲0,表示當前位置安全,能夠擺放皇后。
public int back2Track(int row,int count, int n ,int[] cols,int[] zhu, int[] ci) { for(int col= 0; col< n; col++) { if(isSafe(row,col,n,cols,zhu,ci)) { /** * 在安全位置,佔用 */ //當前列,主對角線,列都佔用標誌:1 cols[col] =1; zhu[row-col + n] =1; ci[row +col] =1; //遍歷到最後一行了,返回成功解一個 if(row +1 ==n ) {count ++;} else { //遍歷下一行,此時 row+1 count = back2Track(row+1,count,n,cols,zhu,ci); } //若是某行全部列都不安全,遞歸回退,將原來置爲1的位置清除,繼續遍歷下一列 cols[col] = 0; zhu[row-col+ n] =0; ci[row+col] =0; } } return count; }
方法二
關鍵方法:
1.利用一維數組表示二維空間的棋盤中皇后的位置
2.利用兩點之間斜率絕對值爲 1,即夾角的正切值爲1,夾角爲 45度或者 135度,據此判斷是否在其餘皇后的攻擊斜線上。
代碼以下:
/** * 表示n*n位棋盤,也表示n位皇后 */ int n ; /** * 一維數組存儲皇后位置,數組下標表明行,數組值表明列;例如: locations[0]= 0: 表示 第一行第一列有一位皇后 */ int[] locations; /** * 記錄可排列種類有多少 */ static int maxCount;
/** * 判斷第k個皇后在第k行某列是否安全 */ private boolean isSafe(int k) { //與前n-1個皇后比較 for(int i = 0 ; i< k; i++) { //這裏較難理解 //1, 由於locations中記錄前k-1行皇后的列擺放位置,數組下標表明行,數組值表明列 //若是locations[i] == locations[k],則表示這一列已經有皇后佔據了,衝突不安全 //2,Math.abs(k- i) == Math.abs(locations[k] - locations[i]) 利用的是|y2-y1| = |x2-x1| 公式,斜率爲1,也不安全 if(locations[i] == locations[k] || Math.abs(k- i) == Math.abs(locations[k] - locations[i])) { return false; } } return true; }
//k表示第k行,也表示第k個皇后, private void check(int k) { if(k ==n) { print(); //成功計數器+1 maxCount ++; return; } for (int i = 0; i<n; i++) { //遍歷列,首先就將當前列設值,在判斷安全時會進行比較 locations[k] =i; if(isSafe(k)) { //若是安全,在遞歸k+1行 check(k+1); } } }
/** * 打印皇后可行排列順序 */ private void print() { for (int col: locations) { System.out.print(col + " "); } System.out.println(); }
一行表明一種擺放方式
輸出結果能夠用遊戲驗證:8皇后遊戲:http://360.6822.com/www1.9/play_76277.html
方法三:利用位運算實現
計算機對位運算計算更快,這個方法實現很巧妙,對位運算會有更深的理解。
在看該方法前,先複習一下位運算的基本運算,後面會用上。
1.按位與 & : 有0則爲0, 只有當兩位都是 1 時結果纔是 1,不然爲0。 2.按位或 | : 有1則爲1,即兩位中只要有 1 位爲 1 結果就是 1,兩位都爲 0 則結果爲 0。 3. 取反 ~ : 0 則變爲 1,1 則變爲 0。 4.左位移 << :向左進行移位操做,高位丟棄,低位補 0 如 1<<3 1向左位移3位 000000001 --> 00001000 = 2^3 =8 (10) 5.右位移 >> :向右進行移位操做,對無符號數,高位補 0,對於有符號數,高位補符號位 如 00001011 向右位移2位,00001011>>2 = 00000010, 左邊2位丟棄,高位補0
示例圖以下:
與運算
或運算
1.原碼:是最簡單的機器數表示法。用最高位表示符號位,‘1’表示負號,‘0’表示正號。其餘位存放該數的二進制的絕對值。
2.反碼:正數的反碼仍是等於原碼,負數的反碼就是他的原碼除符號位外,按位取反。
3.補碼: 正數的補碼等於他的原碼,負數的補碼等於反碼+1。
注:正數的原碼,反碼,補碼相同,計算機中運算是以補碼的形式存儲計算的。
本次算法相關:
提示: 該算法遍歷是從低位開始,是從右到左的遍歷,上2個方法是從左到右的方式,這個方法巧妙在於以前的方法是用數組存儲被佔用的位置,這個就是用3個int類型值,int 4個字節,32位來替代數組表示。
下面咱們來看如何使用3個int類型的值 表示列,主對角線,次對角線佔用信息的
int column // 比特位記錄列被佔用信息,以下圖: 1001000表示 第1列和第4列被佔用
int pie //表示左斜線,即次對角線的佔用信息, 0010000,傳遞到第3行時,表示第3行3列位置不安全。
int na // 表示右斜線,即主對角線的佔用信息,00101000
利用位運算 或,便可求得下一行的全部被佔用的狀況,運算結果以下圖:
10111000 ,三個變量進行或運算,求出 1,3,4,5位置會被攻擊,爲0的位置是能夠擺放皇后的
左斜線(pie)傳遞到下一行的約束推導,以下圖, pie 與 當前行放置皇后位置求並集,再向左位移1位,即傳遞到下一行的左斜線約束
,同理,右斜線(na)求並集向右位移1位
因此公式以下:
p: 表明該行皇后擺放的位置,如 00000001
肯定下一行能夠哪些位置能夠擺放皇后: cloumn | p
獲取下一行左下斜被佔用的狀況: (pie | p) << 1
獲取下一行右下斜被佔用的狀況: (na | p) >> 1
清除該行擺放皇后的位置: bits = bits & (bits - 1)
代碼以下:
int totalNQueens(int n) { return backTrack(0,0,0,0,n); } //用於記錄一共有多少種方式 private int count; public int backTrack(int row, int column, int pie, int na,int n) { if(row == n) { count++; return count; } /**下一行的可擺放的位置,1:表明能夠擺放;0:不能夠 * (column | pie | na),表示該行能夠擺放的位置,此時 0 表明能夠擺放,~ 取反方便下一步操做,1表明能夠擺放 * 可是int類型 32 位,取反高位爲1了,不須要,所以 (1<<n -1)按位與,抹去高位爲0,只留下須要的n個低位 * 這裏爲何要取反,1表示能夠擺放的位置了,是爲了方便bits與0比較 */ int bits = ~(column | pie | na) & ((1 << n) - 1); /** * 大於0,表示存在1,有能夠擺放皇后的位置 */ while (bits > 0) { /** * 1,取出該行最右邊的爲1的那一位,表示能夠擺放 * 涉及到計算機存儲的是補碼問題 */ int p = bits & -bits; backTrack(row + 1, column | p, (pie | p) << 1, (na | p) >> 1, n); /** * 抹去最右邊爲1的那位,將1變爲0,繼續從右遍歷第二位爲1的 */ bits = bits & (bits - 1); } return count; }