【戀上數據結構】回溯、剪枝(八皇后、n皇后)、LeetCode51.N皇后、LeetCode52.N皇后 II

回溯(Back Tracking)

回溯可以理解爲:通過選擇不同的岔路口來通往目的地(找到想要的結果)

  • 每一步都選擇一條路出發,能進則進,不能進則退回上一步(回溯),換一條路再試

樹的先序遍歷圖的深度優先搜索(DFS)八皇后走迷宮都是典型的回溯應用

下圖中紅色代表實際路線綠色代表回溯
在這裏插入圖片描述
不難看出來,回溯很適合使用遞歸

提出八皇后問題(Eight Queens)

在這裏插入圖片描述

初步思路一:暴力出奇跡

從 64 個格子中選出任意 8 個格子擺放皇后,檢查每一種擺法的可行性
一共 C648C_{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];
    }
}

真就面向測試案例編程啊!
在這裏插入圖片描述

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