回溯
数据结构与算法笔记:恋上数据结构笔记目录
回溯(Back Tracking)
回溯可以理解为:通过选择不同的岔路口来通往目的地(找到想要的结果)
- 每一步都选择一条路出发,能进则进,不能进则退回上一步(回溯),换一条路再试
树的先序遍历、图的深度优先搜索(DFS)、八皇后、走迷宫都是典型的回溯应用
下图中红色代表实际路线,绿色代表回溯。
不难看出来,回溯很适合使用递归。
提出八皇后问题(Eight Queens)
初步思路一:暴力出奇迹
从 64 个格子中选出任意 8 个格子摆放皇后,检查每一种摆法的可行性
一共
C
64
8
C_{64}^{8}
C648 种摆法(大约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];
}
}
真就面向测试案例编程啊!