回溯
回溯法的基本思想:回溯法在包含问题的所有可能解的解空间树中,从根结点出发,按照深度优先的策略进行搜索,对于解空间树的某个结点,如果该结点满足问题的约束条件,则进入该子树继续进行搜索,否则将以该结点为根结点的子树进行剪枝。
回溯法的算法框架按照问题的解空间一般分为子集树算法框架与排列树算法框架。
-
当给定的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树。
-
当给定的问题是确定 n 个元素满足某种性质的排列时,对应的解空间树称为排列树;排列树通常有n!个叶子结点。
回溯法解题的关键要素:
- 针对给定的问题,定义问题的解空间
- 确定易于搜索的解空间结构
- 以深度优先方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索
1. 迷宫回溯问题
import java.util.Random;
// 迷宫回溯问题
public class MazeBack {
public static void main(String[] args) {
// 先创建一个二维数组,模拟迷宫地图
int[][] map = new int[9][9];
layoutMaze(map, 9); // 给迷宫布局
// 使用递归回溯找通路
boolean success = setWay(map, 1, 1);
if (success)
System.out.println("找到通路:");
else
System.out.println("没有通路:");
display(map); // 显示地图
}
/**
* 从左上角开始出发,右下角为终点
* 0表示没走过;1表示墙;2表示通路;3表示已经走过但走不通
* 寻路策略:下-->右-->上-->左,如果走不通,再回溯
*
* @param map 表示地图
* @param i 表示起始位置
* @param j 表示从起始位置
* @return 如果找到通路,则返回true,否则返回false
*/
public static boolean setWay(int[][] map, int i, int j) {
if (map[map.length - 2][map[0].length - 2] == 2) {
// 递归终止条件,通路已经找到
return true;
} else if (map[i][j] == 0) { // 如果当前这个点还没走过
map[i][j] = 2; // 假定该点可以走通
if (setWay(map, i + 1, j)) { // 向下走
return true;
} else if (setWay(map, i, j + 1)) { // 向右走
return true;
} else if (setWay(map, i - 1, j)) { // 向上走
return true;
} else if (setWay(map, i, j - 1)) { // 向左走
return true;
} else { // 该点走不通,是死路
map[i][j] = 3;
return false;
}
} else {
// 1:走不通
// 2:走过了,不用再走
// 3:走过了走不通
// 因此如果map[i][j] != 0,说明该点不用走,直接返回false
return false;
}
}
// 初始化地图
public static void initMap(int[][] map) {
for (int r = 0; r < map.length; r++) {
for (int c = 0; c < map[0].length; c++) {
// 将地图四周设为1
if (r == 0 || r == map.length - 1 || c == 0 || c == map[0].length - 1)
map[r][c] = 1;
else
map[r][c] = 0;
}
}
}
// 给迷宫布局
public static void layoutMaze(int[][] map, int total) {
initMap(map); // 先初始化地图
for (int i = 0; i < total; i++) {
Random random = new Random();
int r = random.nextInt(map.length - 2) + 1;
int c = random.nextInt(map[0].length - 2) + 1;
map[r][c] = 1;
}
}
// 显示地图
public static void display(int[][] map) {
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[0].length; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
}
2. 八皇后问题
八皇后问题递归思路:
- 第一个皇后先放第一列第一行
- 第二个皇后放第二行第一列,然后判断是否OK,如果不OK,继续放第二列、第三列…依次把所有列放完,找到一个合适的。
- 继续放第三个皇后,还是第一列、第二列…直到第8个皇后也放在一个不冲突的位置,算找到了一个正确解。
- 当得到一个正确解时,在栈回退到上一个栈时,就开始回溯,即得到第一个皇后放在第一列的所有正确解。
- 然后回头继续第一个皇后放第二列,后面继续循环执行1,2,3,4步骤。
- 理论上应该创建一个二维数组表示棋盘,但实际上通过算法,用一个一维数组即可(下标为行,值为列)。
八皇后问题实现:
import java.util.Arrays;
// 八皇后问题递归实现
public class NQueens {
static int max = 8; // 一共有几个皇后
static int[] arr = new int[max]; // 保存一个结果
static int total = 0;
static int judgeCount = 0;
public static void main(String[] args) {
check(0);
System.out.println(max + "个皇后解法:" + total); // 92
System.out.println("判断次数:" + judgeCount); // 15720
}
// 放置第n个皇后
public static void check(int n) {
if (n == max) {
// max个皇后已经放好,显示结果
total++;
System.out.println(Arrays.toString(arr));
return;
}
// 依次放入皇后,并判断是否冲突
for (int i = 0; i < max; i++) {
// 先把当前皇后n放到该行第一列
arr[n] = i;
// 判断第n个皇后在i位置是否冲突
if (Conflict(n) == false) { // 不冲突
// 接着放n+1个皇后,即开始递归
check(n + 1);
}
// 如果冲突,继续循环,将第n个皇后后移一列
}
}
// 检测第n个皇后是否与已摆放的皇后冲突
public static boolean Conflict(int n) {
judgeCount++;
for (int i = 0; i < n; i++) {
// 检测是否在同一列或同一斜线(正方形长宽相等)
if (arr[i] == arr[n] || Math.abs(n - i) == Math.abs(arr[n] - arr[i]))
return true;
}
return false;
}
}
3. 马踏棋盘问题
马踏棋盘问题也称骑士周游问题,是旅行商问题(TSP)或哈密顿回路问题(HCP)的一个特例。在8×8的国际象棋棋盘上,用一个马跟随马步跳遍整个棋盘,要求每个格子都只跳到一次,最后回到出发点。这是一个NP问题,通常采用回溯法或启发式搜索类算法转化。
马踏棋盘问题解决思路:
- 将当前位置设置为已经访问,然后根据当前位置,计算还能走哪些位置,并将这些位置放入到一个集合中;最多有8个位置,每走一步就让step增1
- 遍历集合中存放的所有位置,看哪个可以走通;如果走通,就继续,走不通,就回溯
- 使用step和应该走的步数比较,来判断应该走的步数是否走完了;若不相等,则将整个棋盘置0
- 不同的走法(策略),得到的结果可能不同,效率也会有影响(可优化)
马在当前位置可以踏的位置:
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
public class Backtracking {
static int x = 8; // 棋盘行
static int y = 8; // 棋盘列
static int[][] chessboard = new int[x][y]; // 棋盘
static boolean[] visited = new boolean[x * y]; // 标记棋盘的各个位置是否访问过
static boolean finished = false; // 应该走的步数是否走完了
// 回溯法解决马踏棋盘问题
public static void knightTour(int row, int column, int step) {
// 第step步访问该位置
chessboard[row][column] = step;
// 将该位置标记为已访问
visited[row * x + column] = true;
// 根据当前位置,计算还能走哪些位置
List<Point> next = next(new Point(column, row));
/**
* 对next按照可走步数进行升序排序,减少回溯的次数:
* 不同的走法(策略),得到的结果可能不同
* 贪心选择策略:选择可走步数最少的位置
*/
next.sort(((o1, o2) -> next(o1).size() - next(o2).size()));
// 遍历每一个还能走哪些位置
for (Point point : next) {
// 如果该位置还没有被访问,则访问这个位置
if (!visited[point.y * x + point.x])
knightTour(point.y, point.x, step + 1);
}
// 判断棋盘是否踏完,如果没踏完,将整个棋盘置0
// step < x * y - 1有两种情况:1. 棋盘还没踏完 2. 棋盘正在回溯
if (step < x * y - 1 && !finished) {
chessboard[row][column] = 0;
visited[row * x + column] = false;
} else {
finished = true;
}
}
// 根据当前位置(Point为Java的内置对象),计算还能走哪些位置,最多有8个位置
// 返回还能走的位置的坐标集合
private static List<Point> next(Point current) {
List<Point> points = new ArrayList<>();
Point point = new Point();
// 可以走0这个位置
if ((point.x = current.x + 2) < x && (point.y = current.y - 1) >= 0)
points.add(new Point(point));
// 可以走1这个位置
if ((point.x = current.x + 2) < x && (point.y = current.y + 1) < y)
points.add(new Point(point));
// 可以走2这个位置
if ((point.x = current.x + 1) < x && (point.y = current.y + 2) < y)
points.add(new Point(point));
// 可以走3这个位置
if ((point.x = current.x - 1) >= 0 && (point.y = current.y + 2) < y)
points.add(new Point(point));
// 可以走4这个位置
if ((point.x = current.x - 2) >= 0 && (point.y = current.y + 1) < y)
points.add(new Point(point));
// 可以走5这个位置
if ((point.x = current.x - 2) >= 0 && (point.y = current.y - 1) >= 0)
points.add(new Point(point));
// 可以走6这个位置
if ((point.x = current.x - 1) >= 0 && (point.y = current.y - 2) >= 0)
points.add(new Point(point));
// 可以走7这个位置
if ((point.x = current.x + 1) < x && (point.y = current.y - 2) >= 0)
points.add(new Point(point));
return points;
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
knightTour(0, 0, 0);
long end = System.currentTimeMillis();
System.out.println("马踏棋盘耗时" + (end - start) + "毫秒,结果为:");
for (int r = 0; r < chessboard.length; r++) {
for (int c = 0; c < chessboard[r].length; c++) {
System.out.printf("%4d", chessboard[r][c]);
}
System.out.println();
}
}
}