引言
最近在刷leetcode演算法題的時候,51題很有意思;
題目是這樣的:
n 皇后問題 研究的是如何將 n 個皇后放置在 n×n 的棋盤上,並且使皇后彼此之間不能相互攻擊。
給你一個整數 n ,返回所有不同的 n 皇后問題 的解決方案。
每一種解法包含一個不同的 n 皇后問題 的棋子放置方案,該方案中 'Q' 和 '.' 分別代表了皇后和空位。
示例 1:
輸入:n = 4
輸出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解釋:如上圖所示,4 皇后問題存在兩個不同的解法。
示例 2:
輸入:n = 1
輸出:[["Q"]]
提示:
1 <= n <= 9
皇后彼此不能相互攻擊,也就是說:任何兩個皇后都不能處於同一條橫行、縱行或斜線上。
來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/n-queens
著作權歸領釦網路所有。商業轉載請聯絡官方授權,非商業轉載請註明出處。
這道題有意思地方在哪裡,是它需要你算錯所有的可能性;而不是其中的1種或者2種可能性;
看到這道題想起只有在遊戲內做過的消除演算法判定方案;《小遊戲五子連珠消除解決方案》在這片文章中,我只是去驗算記憶體中能消除的物件,也就是查詢所有已經出現的物件進行判定;
這道題是讓你從未知的開始,去填充資料,然後找出所有可能性;
題解1
第一遍讀完題目的時候,我盲目的自以為然的寫了一個演算法;
/** * @author: Troy.Chen(失足程式設計師, 15388152619) * @version: 2021-07-08 10:53 **/ class Solution { public List<List<String>> solveNQueens(int n) { List<List<String>> ret = new ArrayList<>(); List<boolean[][]> ot = new ArrayList<>(); for (int z = 0; z < n; z++) { for (int x = 0; x < n; x++) { boolean[][] action = action(ot, n, x, z); if (action == null) { continue; } List<String> item = new ArrayList<>(); for (boolean[] booleans : action) { String str = ""; for (boolean a : booleans) { str += a ? "Q" : "."; } item.add(str); } ret.add(item); } } return ret; } public boolean[][] action(List<boolean[][]> ot, int n, int startX, int startZ) { boolean[][] tmp = new boolean[n][n]; tmp[startZ][startX] = true; int qN = 1; for (int z = 0; z < tmp.length; z++) { for (int x = 0; x < tmp.length; x++) { if (check(tmp, x, z)) { tmp[z][x] = true; qN++; } } } if (qN >= n) { if (!ot.isEmpty()) { for (boolean[][] tItem : ot) { boolean check = true; for (int z = 0; z < tmp.length; z++) { for (int x = 0; x < tmp.length; x++) { if (tmp[z][x]) { if (tmp[z][x] != tItem[z][x]) { check = false; break; } } } if (!check) { break; } } if (check) { return null; } } } ot.add(tmp); return tmp; } else { return null; } } public boolean check(boolean[][] tmp, int checkx, int checkz) { /*檢查橫向*/ for (int x = 0; x < tmp.length; x++) { if (tmp[checkz][x]) { return false; } } /*檢查縱向*/ for (int z = 0; z < tmp.length; z++) { if (tmp[z][checkx]) { return false; } } int tx; int tz; { /*從左到右,從上到下*/ tx = checkx; tz = checkz; while (true) { if (tmp[tz][tx]) { return false; } tx--; tz--; if (tx < 0 || tz < 0) { break; } } tx = checkx; tz = checkz; while (true) { if (tmp[tz][tx]) { return false; } tx++; tz++; if (tx >= tmp.length || tz >= tmp.length) { break; } } } { /*從右到左,從上到下*/ tx = checkx; tz = checkz; while (true) { if (tmp[tz][tx]) { return false; } tx++; tz--; if (tx >= tmp.length || tz < 0) { break; } } tx = checkx; tz = checkz; while (true) { if (tmp[tz][tx]) { return false; } tx--; tz++; if (tx < 0 || tz >= tmp.length) { break; } } } return true; } }
這個寫法,是我腦袋裡的第一想法,程式碼怎麼理解呢;
就是說我從第一個位置開始去擺放皇后,然後依次去遞推其他格子皇后擺放位置,然後查詢皇后數量是否符合提議;自己測試了即便就感覺對了,在leetcode提交程式碼
提交程式碼過後啪啪打臉了,失敗了
檢視執行輸出,當輸入n=6的時候;
leetcode的輸出結果是有4種可能性,但是我輸出可能性只有1種;
對比之前五子棋消除方案來講;check程式碼無疑是沒有問題的,起碼我在對於縱向橫向,斜45度檢查方案是沒問題的;能保住輸出是符合要求的,
那麼問題就在於查詢可能性程式碼;我們能找出符合條件的可能性,只是沒有辦法去找出所有的可能性
仔細分析了一下,不難看出完整的迴圈一次,得出的結果其實是固定;
怎麼理解呢,因為每一次for迴圈數值都是從0開始推導;那麼找到第一個符合條件的格子就落子了,所以得到的結果總是一致的;
既然我們需要算出所有組合;
那麼我們能有什麼樣的方式去完成這個組合求解呢?
題解2
思考了良久;既然正常的迴圈其實無法查詢每一種可能性,只能放棄正常迴圈;
思考了一下能不能按照當前行,每一個格子,依次往下逐行去掃描每一種可能性;
如果我們要這樣去逐行掃描,我們就得在初始化棋盤格子到時候擺放上,上一行的情況才能去推算出當前行能在那些地方擺放;
當前行的遞推的時候也是按照第一個格子到最後一個格子去驗算能擺放的格子,然後查詢到能擺放的格子,就把現在的快照資料提交給下一行進行推算;
這樣的做法就是,當第一行開始判斷是,第一個格子肯定能擺放棋子對吧,然後把第一個格子擺放上棋子過後,把棋盤直接推給第二行,
第二行拿到棋盤過後,去查詢能擺放的格子,每查詢到能擺放的格子就把棋盤拷貝一次副本,然後設定當前查詢的格子落子,然後把副本棋盤傳遞給下一行;
這樣依次類推,如果能達到最後一行,並且找到適合的格子,那麼就是一種可能性,
我們在遞推的同時,每一次都會記錄自己的進度,再次往下遞推;
這麼說可能有些抽象;來看一下圖
我們的遞推過程就是,從a1開始,每一次找到合適的格子就把棋盤拷貝一次,傳遞給下一行(b),然後b開始從第一個格子開始遞推,找到符合規則的格子就再一次拷貝棋盤傳遞給C;
這樣的話,就是完完整的逐行掃描了,所有的肯能行;
來看一下帶實現
/** * @author: Troy.Chen(失足程式設計師, 15388152619) * @version: 2021-07-08 10:53 **/ class Solution { public List<List<String>> solveNQueens(int n) { List<List<String>> ret = new ArrayList<>(); List<boolean[][]> ot = new ArrayList<>(); boolean[][] tmp = new boolean[n][n]; check(ot, tmp, 0, 0); for (boolean[][] a : ot) { ret.add(convert(a)); } return ret; } /*按照規定轉化字串*/ public List<String> convert(boolean[][] tmp) { List<String> item = new ArrayList<>(); for (boolean[] booleans : tmp) { String str = ""; for (boolean a : booleans) { str += a ? "Q" : "."; } item.add(str); } return item; } public void check(List<boolean[][]> ot, boolean[][] tmp, int checkx, int checkz) { for (int x = checkx; x < tmp.length; x++) { if (check0(tmp, x, checkz)) { /*相當於逐行進行掃描所以要拷貝程式碼*/ int tmpz = checkz; boolean[][] clone = clone(tmp); clone[tmpz][x] = true; tmpz++; if (tmpz < tmp.length) { check(ot, clone, 0, tmpz); } else { ot.add(clone); } } } } /*拷貝陣列*/ public boolean[][] clone(boolean[][] tmp) { boolean[][] clone = tmp.clone(); for (int i = 0; i < tmp.length; i++) { clone[i] = tmp[i].clone(); } return clone; } public boolean check0(boolean[][] tmp, int checkx, int checkz) { /*檢查橫向*/ for (int x = 0; x < tmp.length; x++) { if (tmp[checkz][x]) { return false; } } /*檢查縱向*/ for (int z = 0; z < tmp.length; z++) { if (tmp[z][checkx]) { return false; } } int tx; int tz; { /*從左到右,從上到下*/ tx = checkx; tz = checkz; while (true) { if (tmp[tz][tx]) { return false; } tx--; tz--; if (tx < 0 || tz < 0) { break; } } tx = checkx; tz = checkz; while (true) { if (tmp[tz][tx]) { return false; } tx++; tz++; if (tx >= tmp.length || tz >= tmp.length) { break; } } } { /*從右到左,從上到下*/ tx = checkx; tz = checkz; while (true) { if (tmp[tz][tx]) { return false; } tx++; tz--; if (tx >= tmp.length || tz < 0) { break; } } tx = checkx; tz = checkz; while (true) { if (tmp[tz][tx]) { return false; } tx--; tz++; if (tx < 0 || tz >= tmp.length) { break; } } } return true; } }
再次提交程式碼;
程式碼執行通過了,得到的結論也是一樣的了;
但是不幸的是我的程式碼只擊敗了8%的使用者,希望得到各位園友效能更高效的演算法;
總結
這樣的遞推方式,其實可以實現像五子棋AI演算法;通過落子情況去遞推在哪裡下子可以得到五個棋子在一條線上;
只是遞推開上時棋盤已經定下了落子情況;從當前的落子情況去遞推他的可能性;