广度优先算法(BFS)
广度优先搜索算法(又称宽度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。
广度优先算法的核心思想是:从初始节点开始,应用算符生成第一层节点,检查目标节点是否在这些后继节点中,若没有,再用产生式规则将所有第一层的节点逐一扩展,得到第二层节点,并逐一检查第二层节点中是否包含目标节点。若没有,再用算符逐一扩展第二层的所有节点……,如此依次扩展,检查下去,直到发现目标节点为止。
思路与实现
1. 初始定义和数据结构
class PuzzleState {
int[][] board; // 棋盘状态
int emptyX, emptyY; // 空白格子的坐标
PuzzleState parent; // 父状态,用于记录路径
public PuzzleState(int[][] board, int emptyX, int emptyY, PuzzleState parent) {
this.board = new int[board.length][board[0].length];
for (int i = 0; i < board.length; i++) {
System.arraycopy(board[i], 0, this.board[i], 0, board[0].length);
}
this.emptyX = emptyX;
this.emptyY = emptyY;
this.parent = parent;
}
public boolean isGoal(int[][] targetBoard) {
return Arrays.deepEquals(this.board, targetBoard);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PuzzleState that = (PuzzleState) o;
return Arrays.deepEquals(board, that.board);
}
@Override
public int hashCode() {
return Arrays.deepHashCode(board);
}
}
PuzzleState 类:用于表示每个状态的棋盘配置。'board
' 存储当前的棋盘状态,'emptyX
'和 'emptyY
'记录空白格子的位置,'parent
'记录当前状态的父状态,以便构建路径。
2. 解题方法
solve 方法
public static List<int[][]> solve(int[][] startBoard, int[][] targetBoard) {
// 寻找起始状态中空白格子的位置
int startX = 0, startY = 0;
outerLoop:
for (int i = 0; i < startBoard.length; i++) {
for (int j = 0; j < startBoard[0].length; j++) {
if (startBoard[i][j] == 0) {
startX = i;
startY = j;
break outerLoop;
}
}
}
// 创建起始状态
PuzzleState startState = new PuzzleState(startBoard, startX, startY, null);
Queue<PuzzleState> queue = new LinkedList<>(); // 使用队列进行广度优先搜索
Set<PuzzleState> visited = new HashSet<>(); // 记录访问过的状态,避免重复访问
queue.add(startState);
visited.add(startState);
int nodeCount = 0;
long startTime = System.currentTimeMillis();
while (!queue.isEmpty()) {
PuzzleState currentState = queue.poll();
nodeCount++;
if (currentState.isGoal(targetBoard)) { // 判断是否达到目标状态
long endTime = System.currentTimeMillis();
System.out.println("Solution found in " + (endTime - startTime) + " ms with " + nodeCount + " nodes searched.");
return constructPath(currentState); // 构建路径并返回解决方案
}
// 尝试移动空白格子的四个方向
for (int[] direction : DIRECTIONS) {
int newX = currentState.emptyX + direction[0];
int newY = currentState.emptyY + direction[1];
if (newX >= 0 && newX < startBoard.length && newY >= 0 && newY < startBoard[0].length) {
// 复制当前棋盘状态,并移动空白格子
int[][] newBoard = new int[startBoard.length][startBoard[0].length];
for (int i = 0; i < startBoard.length; i++) {
System.arraycopy(currentState.board[i], 0, newBoard[i], 0, startBoard[0].length);
}
newBoard[currentState.emptyX][currentState.emptyY] = newBoard[newX][newY];
newBoard[newX][newY] = 0;
// 创建新状态并检查是否访问过
PuzzleState newState = new PuzzleState(newBoard, newX, newY, currentState);
if (!visited.contains(newState)) {
queue.add(newState);
visited.add(newState);
}
}
}
}
// 搜索完成仍未找到解决方案
long endTime = System.currentTimeMillis();
System.out.println("No solution found in " + (endTime - startTime) + " ms with " + nodeCount + " nodes searched.");
return null; // 返回空表示无解
}
solve 方法:使用广度优先搜索(BFS)来解决拼图问题。从起始状态开始,逐步尝试所有可能的移动,直到达到目标状态或者所有状态都被搜索过。
3. 解决方案构建
constructPath 方法
private static List<int[][]> constructPath(PuzzleState state) {
List<int[][]> path = new ArrayList<>();
while (state != null) {
path.add(state.board); // 将每个状态的棋盘配置加入路径
state = state.parent; // 往回追溯路径
}
Collections.reverse(path); // 反转路径,使起始状态在最前面
return path;
}
使用广度优先搜索算法来解决拼图问题,通过不断尝试所有可能的移动,逐步接近目标状态。每个状态的棋盘配置和移动过程都被封装在 PuzzleState
类中,确保了状态的独立性和可追溯性。解决方案的构建通过回溯父状态的方式实现,最终输出完整的解决路径。
算法可视化
使用stdDraw绘图实现算法的可视化
在 PuzzlePanel
的 paintComponent()
方法中,只绘制发生变化的部分,而不是重新绘制整个棋盘。这样可以提升绘制效率和动画的流畅度。同时,我们使用 javax.swing.Timer 来实现动画的更新。在 startAnimation() 方法中,设置一个定时器,每隔一段时间更新一次 PuzzlePanel 的绘制内容,实现逐帧动画效果。此外javax.swing.Timer确保了与Swing的事件分发线程(EDT)兼容,并正确执行UI更新。
实现效果
已实现的迭代版本
同时保存多个棋盘以供选择
import java.awt.*;
import java.util.*;
import javax.swing.*;
class PuzzleState {
int[][] board;
int emptyX, emptyY;
PuzzleState parent;
public PuzzleState(int[][] board, int emptyX, int emptyY, PuzzleState parent) {
this.board = new int[board.length][board[0].length];
for (int i = 0; i < board.length; i++) {
System.arraycopy(board[i], 0, this.board[i], 0, board[0].length);
}
this.emptyX = emptyX;
this.emptyY = emptyY;
this.parent = parent;
}
public boolean isGoal(int[][] targetBoard) {
return Arrays.deepEquals(this.board, targetBoard);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PuzzleState that = (PuzzleState) o;
return Arrays.deepEquals(board, that.board);
}
@Override
public int hashCode() {
return Arrays.deepHashCode(board);
}
}
public class SlidingPuzzleSolver {
private static final int DIM = 4; // 4x4 board
private static final int TILE_SIZE = 100; // Size of each tile in pixels
private static final int BOARD_SIZE = DIM * TILE_SIZE;
private static final int[][] DIRECTIONS = {
{1, 0}, {-1, 0}, {0, 1}, {0, -1}
};
private int[][] board;
private java.util.List<int[][]> solutionSteps;
private JFrame frame;
private PuzzlePanel puzzlePanel;
private int stepIndex;
private javax.swing.Timer animationTimer; // Explicitly use javax.swing.Timer
public SlidingPuzzleSolver(java.util.List<int[][]> solutionSteps) {
this.solutionSteps = solutionSteps;
this.board = solutionSteps.get(0);
this.stepIndex = 0;
// Initialize and show GUI
SwingUtilities.invokeLater(() -> {
frame = new JFrame("Sliding Puzzle Solver");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
puzzlePanel = new PuzzlePanel();
frame.add(puzzlePanel);
frame.pack();
frame.setVisible(true);
startAnimation();
});
}
private void startAnimation() {
animationTimer = new javax.swing.Timer(500, e -> {
if (stepIndex < solutionSteps.size()) {
board = solutionSteps.get(stepIndex++);
puzzlePanel.repaint(); // Trigger repaint to update the puzzle panel
} else {
animationTimer.stop();
}
});
animationTimer.setInitialDelay(0);
animationTimer.start();
}
private class PuzzlePanel extends JPanel {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
for (int i = 0; i < DIM; i++) {
for (int j = 0; j < DIM; j++) {
if (board[i][j] != 0) {
drawTile(g, j * TILE_SIZE, i * TILE_SIZE, board[i][j]);
}
}
}
}
private void drawTile(Graphics g, int x, int y, int value) {
g.setColor(Color.LIGHT_GRAY);
g.fillRect(x, y, TILE_SIZE, TILE_SIZE);
g.setColor(Color.BLACK);
g.drawRect(x, y, TILE_SIZE, TILE_SIZE);
g.setColor(Color.BLACK);
g.setFont(new Font("Arial", Font.BOLD, 24));
g.drawString(String.valueOf(value), x + TILE_SIZE / 2 - 10, y + TILE_SIZE / 2 + 10);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(BOARD_SIZE, BOARD_SIZE);
}
}
public static java.util.List<int[][]> solve(int[][] startBoard, int[][] targetBoard) {
int startX = 0, startY = 0;
outerLoop:
for (int i = 0; i < startBoard.length; i++) {
for (int j = 0; j < startBoard[0].length; j++) {
if (startBoard[i][j] == 0) {
startX = i;
startY = j;
break outerLoop;
}
}
}
PuzzleState startState = new PuzzleState(startBoard, startX, startY, null);
Queue<PuzzleState> queue = new LinkedList<>();
Set<PuzzleState> visited = new HashSet<>();
queue.add(startState);
visited.add(startState);
int nodeCount = 0;
long startTime = System.currentTimeMillis();
while (!queue.isEmpty()) {
PuzzleState currentState = queue.poll();
nodeCount++;
if (currentState.isGoal(targetBoard)) {
long endTime = System.currentTimeMillis();
System.out.println("Solution found in " + (endTime - startTime) + " ms with " + nodeCount + " nodes searched.");
return constructPath(currentState);
}
for (int[] direction : DIRECTIONS) {
int newX = currentState.emptyX + direction[0];
int newY = currentState.emptyY + direction[1];
if (newX >= 0 && newX < startBoard.length && newY >= 0 && newY < startBoard[0].length) {
int[][] newBoard = new int[startBoard.length][startBoard[0].length];
for (int i = 0; i < startBoard.length; i++) {
System.arraycopy(currentState.board[i], 0, newBoard[i], 0, startBoard[0].length);
}
newBoard[currentState.emptyX][currentState.emptyY] = newBoard[newX][newY];
newBoard[newX][newY] = 0;
PuzzleState newState = new PuzzleState(newBoard, newX, newY, currentState);
if (!visited.contains(newState)) {
queue.add(newState);
visited.add(newState);
}
}
}
}
long endTime = System.currentTimeMillis();
System.out.println("No solution found in " + (endTime - startTime) + " ms with " + nodeCount + " nodes searched.");
return null;
}
private static java.util.List<int[][]> constructPath(PuzzleState state) {
java.util.List<int[][]> path = new ArrayList<>();
while (state != null) {
path.add(state.board);
state = state.parent;
}
Collections.reverse(path);
return path;
}
public static void main(String[] args) {
int[][] startBoard = {
{2, 3, 4, 8},
{5, 6, 1, 12},
{9, 10, 7, 0},
{13, 14, 11, 15}
};
int[][] targetBoard = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
java.util.List<int[][]> solution = solve(startBoard, targetBoard);
if (solution != null) {
new SlidingPuzzleSolver(solution);
} else {
System.out.println("No solution found.");
}
}
}