回溯
回溯(Back Tracking)
回溯可以理解爲:通過選擇不同的岔路口來通往目的地(找到想要的結果)
- 每一步都選擇一條路出發,能進則進,不能進則退回上一步(回溯),換一條路再試
樹的先序遍歷、圖的深度優先搜索(DFS)、八皇后、走迷宮都是典型的回溯應用
下圖中紅色代表實際路線,綠色代表回溯。
不難看出來,回溯很適合使用遞歸。
提出八皇后問題(Eight Queens)
初步思路一:暴力出奇跡
從 64 個格子中選出任意 8 個格子擺放皇后,檢查每一種擺法的可行性
一共 種擺法(大約4.4 * 109 種擺法)
初步思路二:根據題意減少暴力程度
很顯然,每一行只能放一個皇后,所以共有 88 種擺法(16777216 種),檢查每一種擺法的可行性
初步思路三:回溯法(回溯+剪枝)
在解決八皇后問題之前,可以先縮小數據規模,看看如何解決四皇后問題
四皇后 - 回溯法圖示
每次走到死路:
- 回溯到上次路口,走另一條路;
如果上次路口的全部路都是死路:- 回溯到上上次路口…
回溯途中夾雜着剪枝操作:即不走確定是死路的路,走到每個路口的時候,先判斷一下這條路口的路中哪些確定是死路,就可以直接跳過這些路。
八皇后 - 回溯法圖示
n皇后實現
合法性檢查
// 存放每一個皇后的列號(在第幾列)
// cols[row] = col 表示第col行第row列擺放了皇后
int[] cols;
// 一共有多少種合理的擺法
int ways = 0;
/**
* 判斷第row行第col列是否可以擺放皇后
*/
boolean isValid(int row, int col) {
for (int i = 0; i < row; i++) {
// 第col行第row列已經擺放了皇后
if (cols[i] == col) return false;
// 第i行的皇后根第row行第col列格子處在同一斜線上
// 45度角斜線: y-y0 = (x-x0), 則 (y-y0)/(x-x0) = 1, 表示爲45度角的斜線
if (Math.abs(col - cols[i]) == row -i) return false;
}
return true;
}
從某一行開始擺放皇后
/**
* 從第 row 行開始擺放皇后
*/
void place(int row) {
// 如果已經放到了第n行,說明找到了一種n皇后的解法
if (row == cols.length) {
ways++;
return;
}
for (int col = 0; col < cols.length; col++) {
if (isValid(row, col)) {
// 將row行col列擺放上皇后
cols[row] = col;
place(row + 1);
}
}
}
擺放所有皇后
/**
* n皇后, 擺放所有皇后
*/
void placeQueens(int n) {
if (n < 1) return;
// 初始化
cols = new int[n];
place(0); // 從第0行開始放置皇后
System.out.println(n + "皇后一共有" + ways + "種擺法");
}
打印
void show() {
for (int row = 0; row < cols.length; row++) {
for (int col = 0; col < cols.length; col++) {
if (cols[row] == col) { // 擺放了皇后
System.out.print("1 ");
} else {
System.out.print("0 ");
}
}
System.out.println();
}
System.out.println("--------------------------");
}
n皇后 - 完整實現
public class Queens {
public static void main(String[] args) {
new Queens().placeQueens(8);
}
// cols[row] = col 表示第col行第row列擺放了皇后
int[] cols;
// 一共有多少種合理的擺法
int ways = 0;
/**
* n皇后, 擺放所有皇后
*/
void placeQueens(int n) {
if (n < 1) return;
// 初始化
cols = new int[n];
place(0); // 從第0行開始放置皇后
System.out.println(n + "皇后一共有" + ways + "種擺法");
}
/**
* 從第 row 行開始擺放皇后
*/
void place(int row) {
// 如果已經放到了第n行,說明找到了一種n皇后的解法
if (row == cols.length) {
ways++;
return;
}
for (int col = 0; col < cols.length; col++) {
if (isValid(row, col)) {
// 將row行col列擺放上皇后
cols[row] = col;
place(row + 1);
}
}
}
/**
* 判斷第row行第col列是否可以擺放皇后
*/
boolean isValid(int row, int col) {
for (int i = 0; i < row; i++) {
// 第col行第row列已經擺放了皇后
if (cols[i] == col) return false;
// 第i行的皇后根第row行第col列格子處在同一斜線上
// 45度角斜線: y-y0 = (x-x0), 則 (y-y0)/(x-x0) = 1, 表示爲45度角的斜線
if (Math.abs(col - cols[i]) == row -i) return false;
}
return true;
}
}
n皇后優化 - 合法性檢查優化
合法性檢查優化 O(n) -> O(1)
之前的合法性檢查需要通過遍歷數組來實現,現在使用3個boolean
數組分別表示:
- 某一列是否有皇后:
boolean[] cols;
- 某一對角線是否有皇后(左上角->右下角):
boolean[] leftTop;
- 某一對角線是否有皇后 (右上角->左下角):
boolean[] rightTop;
用這個進行合法性檢查只需要 O(1) 的時間複雜度。
需要知道一個小技巧:根據行、列求對角線索引(左上、右上情況不同)
for (int col = 0; col < cols.length; col++) {
// 第col列已經有皇后, 繼續下一輪
if (cols[col]) continue;
int ltIndexl = row - col + cols.length - 1; // 左上角->右下角的對角線索引
// 左上->右下已經有皇后, 繼續下輪
if (leftTop[ltIndexl]) continue;
int rtIndex = row + col; // 右上角->左下角的對角線索引
// 右上->左下已經有皇后, 繼續下輪
if (rightTop[rtIndex]) continue;
// 給該列擺上皇后
cols[col] = leftTop[ltIndexl] = rightTop[rtIndex] = true;
place(row + 1); // 該列擺已經擺好了皇后,繼續下一行
// 這一步很關鍵, 列、對角線都是牽一髮而動全身的影響, 需要重置
cols[col] = leftTop[ltIndexl] = rightTop[rtIndex] = false;
}
完整實現
public class Queens2 {
public static void main(String[] args) {
new Queens2().placeQueens(8);
}
// 該變量不是必須, 僅僅是爲了打印
int[] queens;
// 標記着某一列是否有皇后了
boolean[] cols;
// 標記着某一對角線是否有皇后了(左上角->右下角)
boolean[] leftTop;
// 標記着某一對角線是否有皇后了(右上角->左下角)
boolean[] rightTop;
// 一共有多少種合理的擺法
int ways = 0;
/**
* n皇后
*/
void placeQueens(int n) {
if (n < 1) return;
// 初始化
queens = new int[n];
cols = new boolean[n]; // 總共有n列
leftTop = new boolean[(n << 1) - 1]; // n條對角線
rightTop = new boolean[leftTop.length]; // 上面已經做過一次運算,無需再做
place(0); // 從第0行開始擺放皇后
System.out.println(n + "皇后一共有" + ways + "種擺法");
}
/**
* 從第 row 行開始擺放皇后
*/
void place(int row) {
// 如果已經放到第n行,說明找到了一種n皇后的擺法
if (row == cols.length) {
ways++;
show();
return;
}
for (int col = 0; col < cols.length; col++) {
if (cols[col]) continue; // 第col列已經有皇后, 繼續下一輪
int ltIndexl = row - col + cols.length - 1;
if (leftTop[ltIndexl]) continue;
int rtIndex = row + col;
if (rightTop[rtIndex]) continue;
queens[row] = col;
cols[col] = leftTop[ltIndexl] = rightTop[rtIndex] = true;
place(row + 1); // 這一列擺了皇后,繼續下一列
cols[col] = leftTop[ltIndexl] = rightTop[rtIndex] = false;
}
}
void show() {
for (int row = 0; row < queens.length; row++) {
for (int col = 0; col < queens.length; col++) {
if (queens[row] == col) { // 擺放了皇后
System.out.print("1 ");
} else {
System.out.print("0 ");
}
}
System.out.println();
}
System.out.println("--------------------------");
}
}
n皇后優化 - 位運算
可以利用位運算進一步壓縮八皇后的空間複雜度
/**
* 八皇后優化 - 位運算
*/
public class Queens {
public static void main(String[] args) {
new Queens().place8Queens();
}
// 該變量不是必須, 僅僅是爲了打印
int[] queens;
// 標記着某一列是否有皇后了
// 比如 00100111 代表0、1、2、5列已經有皇后
byte cols; // byte是8位
// 標記着某一對角線是否有皇后了(左上角->右下角)
short leftTop; // short是16位
// 標記着某一對角線是否有皇后了(右上角->左下角)
short rightTop; // short是16位
// 一共有多少種合理的擺法
int ways = 0;
/**
* n皇后
*/
void place8Queens() {
queens = new int[8];
place(0); // 從第0行開始擺放皇后
System.out.println("八皇后一共有" + ways + "種擺法");
}
/**
* 從第 row 行開始擺放皇后
*/
void place(int row) {
// 如果已經放到第8行,說明找到了一種8皇后的擺法
if (row == 8) {
ways++;
show();
return;
}
for (int col = 0; col < 8; col++) {
int colV = 1 << col; // 00000001
if((cols & colV) != 0) continue; // col列已經有皇后
int ltV = 1 << (row - col + 7);
if ((leftTop & ltV) != 0) continue;
int rtV = 1 << (row + col);
if ((rightTop & rtV) != 0) continue;
queens[row] = col;
cols |= colV;
leftTop |= ltV;
rightTop |= rtV;
place(row + 1); // 這一列擺了皇后,繼續下一列
cols &= ~colV;
leftTop &= ~ltV;
rightTop &= ~rtV;
}
}
void show() {
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
if (queens[row] == col) { // 擺放了皇后
System.out.print("1 ");
} else {
System.out.print("0 ");
}
}
System.out.println();
}
System.out.println("--------------------------");
}
}
LeetCode 51.N皇后
leetcode_51_N皇后:https://leetcode-cn.com/problems/n-queens/
class Solution {
public List<List<String>> solveNQueens(int n) {
return placeQueens(n);
}
int[] cols; // cols[row] = col; 表示row行col列擺放了皇后
List<List<String>> queens;
List<List<String>> placeQueens(int n) {
if (n < 1) return null;
cols = new int[n];
queens = new ArrayList<>();
place(0); // 從第0行開始擺
return queens;
}
// 在第row行擺放皇后
void place(int row) {
if (row == cols.length) {
queens.add(put());
return;
}
for (int col = 0; col < cols.length; col++) {
if (isValid(row, col)) {
cols[row] = col; // 擺放皇后
place(row + 1); // 去row+1行擺放皇后
}
}
}
boolean isValid(int row, int col) {
for (int i = 0; i < row; i ++) {
if (cols[i] == col) return false;
// 看作兩個點: (row, col)、(i, cols[i]), 斜率爲1則在斜對角
if (row - i == Math.abs(cols[i] - col)) return false;
}
return true;
}
// 將結果放入List中
List<String> put() {
List<String> list = new ArrayList<>();
StringBuilder sb;
for (int row = 0; row < cols.length; row++) {
sb = new StringBuilder();
for (int col = 0; col < cols.length; col++) {
if (col == cols[row]) {
sb.append("Q");
} else {
sb.append(".");
}
}
list.add(sb.toString());
}
return list;
}
}
leetcode 的排名看看就好,有點玄學
LeetCode 52.N皇后 II
leetcode_52_N皇后 II: https://leetcode-cn.com/problems/n-queens-ii/
class Solution {
public int totalNQueens(int n) {
return placeQueens(n);
}
boolean[] cols; // 列上是否有皇后
boolean[] leftTop; // 對角線左上角->右下角是否有皇后
boolean[] rightTop; // 對角線右上角->左下角是否有皇后
int ways = 0; // 擺列次數
int placeQueens(int n) {
if (n < 1) return 0;
cols = new boolean[n];
leftTop = new boolean[(n << 1) - 1];
rightTop = new boolean[leftTop.length];
place(0);
return ways;
}
void place(int row) {
if (row == cols.length) {
ways++;
return;
}
for (int col = 0; col < cols.length; col++) {
if (cols[col]) continue;
int ltIndex = row - col + cols.length - 1 ;
if (leftTop[ltIndex]) continue;
int rtIndex = row + col;
if (rightTop[rtIndex]) continue;
cols[col] = leftTop[ltIndex] = rightTop[rtIndex] = true;
place(row + 1);
cols[col] = leftTop[ltIndex] = rightTop[rtIndex] = false;
}
}
}
無敵解法
評論區看到這個代碼,把我給整笑了,😄
class Solution{
public int totalNQueens(int n) {
int[] rs = new int[]{0,1,0,0,2,10,4,40,92,352,724,2680};
return rs[n];
}
}
真就面向測試案例編程啊!