引言
IDA(Iterative Deepening A)搜索是A算法与迭代加深深度优先搜索(IDDFS)的结合体,兼具A的最优性和IDDFS的内存高效性。对于状态空间巨大但解深度适中的问题(如15数码、24数码等滑块拼图),IDA是实际求解中最有效的算法之一。本文用Java实现IDA算法求解15数码问题,深入讲解启发式函数设计与迭代加深策略。
问题定义:15数码(15-Puzzle)
一个 4×4 的棋盘上有15个数字方块和1个空白格。每次可以将与空白格相邻的数字移入空白格。目标是从初始状态通过最少步数移动到目标状态。
目标状态:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 0
IDA* 算法原理
核心思想
IDA 用深度优先搜索代替A的优先队列,但限制搜索深度为阈值 f_limit。如果某节点的 f = g + h 超过阈值,则记录最小超出值作为下一轮阈值。
f(n) = g(n) + h(n)
- g(n):从起点到n的实际代价(步数)
- h(n):从n到目标的估计代价(启发函数)
迭代加深过程
- 初始阈值 threshold = h(start)
- 执行深度优先搜索,只扩展 f ≤ threshold 的节点
- 如果找到解,返回
- 否则,将 threshold 更新为本次搜索中遇到的最小 f 值(且大于原threshold)
- 重复步骤2-4
启发式函数:曼哈顿距离
对于每个数字方块,计算其当前位置到目标位置的水平+垂直距离之和。曼哈顿距离是15数码问题的可采纳启发式(admissible heuristic),不会高估实际代价。
h(state) = Σ |当前行 - 目标行| + |当前列 - 目标列|
对于所有非零数字
Java实现
public class IDAStar15Puzzle {
private static final int N = 4; // 棋盘大小
private static final int NN = N * N; // 总格子数
private static final int[] dr = {-1, 1, 0, 0}; // 上、下、左、右
private static final int[] dc = {0, 0, -1, 1};
private static final String[] moveNames = {"Up", "Down", "Left", "Right"};
private int[] board; // 一维表示棋盘
private int blankPos; // 空白格位置
private int[] goalPos; // 每个数字的目标位置(一维)
private int[] path; // 记录移动路径
private boolean solved; // 是否找到解
public IDAStar15Puzzle(int[] initial) {
this.board = initial.clone();
this.goalPos = new int[NN];
this.path = new int[100]; // 最大搜索深度
// 预计算每个数字的目标位置
for (int i = 0; i < NN; i++) {
goalPos[i] = i; // 数字i的目标位置就是i(0在右下角)
}
// 找到空白格位置
for (int i = 0; i < NN; i++) {
if (board[i] == 0) {
blankPos = i;
break;
}
}
}
/**
* 计算曼哈顿距离启发式
*/
private int manhattanDistance() {
int dist = 0;
for (int i = 0; i < NN; i++) {
if (board[i] != 0) {
int targetRow = board[i] / N;
int targetCol = board[i] % N;
int curRow = i / N;
int curCol = i % N;
dist += Math.abs(curRow - targetRow) + Math.abs(curCol - targetCol);
}
}
return dist;
}
/**
* 线性冲突启发式(增强版曼哈顿距离)
* 如果两个方块在同一行/列,且目标也在同一行/列,但顺序相反,
* 则至少还需要额外2步来交换它们
*/
private int linearConflict() {
int dist = manhattanDistance();
// 行冲突
for (int row = 0; row < N; row++) {
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
int pos1 = row * N + i;
int pos2 = row * N + j;
int val1 = board[pos1];
int val2 = board[pos2];
if (val1 != 0 && val2 != 0 &&
val1 / N == row && val2 / N == row &&
val1 > val2) {
dist += 2;
}
}
}
}
// 列冲突
for (int col = 0; col < N; col++) {
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
int pos1 = i * N + col;
int pos2 = j * N + col;
int val1 = board[pos1];
int val2 = board[pos2];
if (val1 != 0 && val2 != 0 &&
val1 % N == col && val2 % N == col &&
val1 > val2) {
dist += 2;
}
}
}
}
return dist;
}
/**
* 可解性判断:计算逆序对数
* 对于N×N拼图,当N为奇数时逆序对数必须为偶数;
* 当N为偶数时,逆序对数奇偶性与空白格从底部数的行号相关
*/
public boolean isSolvable() {
int inversions = 0;
for (int i = 0; i < NN; i++) {
if (board[i] == 0) continue;
for (int j = i + 1; j < NN; j++) {
if (board[j] != 0 && board[i] > board[j]) {
inversions++;
}
}
}
int blankRowFromBottom = N - (blankPos / N);
if (N % 2 == 1) {
return inversions % 2 == 0;
} else {
return (inversions + blankRowFromBottom) % 2 == 1;
}
}
/**
* IDA* 核心搜索
*/
public boolean solve() {
if (!isSolvable()) {
System.out.println("该状态无解!");
return false;
}
int threshold = linearConflict();
System.out.println("初始启发值: " + threshold);
while (!solved && threshold < Integer.MAX_VALUE / 2) {
int newThreshold = search(0, threshold, -1);
if (solved) {
return true;
}
threshold = newThreshold;
System.out.println("提升阈值到: " + threshold);
}
return false;
}
/**
* 深度优先搜索,返回下一个阈值
* @param g 当前深度
* @param threshold 当前阈值
* @param lastMove 上一步移动的反方向(避免立即回退)
* @return 如果找到解返回0;否则返回下一个最小阈值
*/
private int search(int g, int threshold, int lastMove) {
int f = g + linearConflict();
if (f > threshold) {
return f; // 超出阈值,返回当前f值作为候选新阈值
}
// 检查是否到达目标状态
if (linearConflict() == 0) {
solved = true;
System.out.println("找到解!步数: " + g);
printPath(g);
return 0;
}
int minNextThreshold = Integer.MAX_VALUE;
int blankRow = blankPos / N;
int blankCol = blankPos % N;
for (int dir = 0; dir < 4; dir++) {
// 避免立即回退上一步
if (lastMove != -1 && dir == (lastMove ^ 1)) {
continue;
}
int newRow = blankRow + dr[dir];
int newCol = blankCol + dc[dir];
// 边界检查
if (newRow < 0 || newRow >= N || newCol < 0 || newCol >= N) {
continue;
}
int newPos = newRow * N + newCol;
// 执行移动:交换空白格和相邻数字
swap(blankPos, newPos);
blankPos = newPos;
path[g] = dir;
int result = search(g + 1, threshold, dir);
if (solved) {
return 0;
}
if (result < minNextThreshold) {
minNextThreshold = result;
}
// 回溯
blankPos = blankRow * N + blankCol;
swap(blankPos, newPos);
}
return minNextThreshold;
}
private void swap(int i, int j) {
int temp = board[i];
board[i] = board[j];
board[j] = temp;
}
private void printPath(int steps) {
System.out.print("移动序列: ");
for (int i = 0; i < steps; i++) {
System.out.print(moveNames[path[i]] + " ");
}
System.out.println();
}
public void printBoard() {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
System.out.printf("%3d", board[i * N + j]);
}
System.out.println();
}
}
public static void main(String[] args) {
// 一个中等难度的15数码实例(需要约30步)
int[] initial = {
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 0, 12,
13, 14, 11, 15
};
System.out.println("初始状态:");
IDAStar15Puzzle puzzle = new IDAStar15Puzzle(initial);
puzzle.printBoard();
long start = System.currentTimeMillis();
boolean solved = puzzle.solve();
long end = System.currentTimeMillis();
System.out.println("求解结果: " + (solved ? "成功" : "失败"));
System.out.println("耗时: " + (end - start) + " ms");
}
}
启发式函数对比
| 启发式 | 计算复杂度 | 效果 | 说明 |
|---|---|---|---|
| 错位方块数 | O(N²) | 弱 | 非0且不在目标位置的方块数 |
| 曼哈顿距离 | O(N²) | 中等 | 各方块到目标位置的距离和 |
| 线性冲突 | O(N³) | 强 | 曼哈顿距离 + 同行/同列冲突惩罚 |
| 模式数据库 | O(1)查表 | 极强 | 预计算子问题的精确代价 |
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(b^d) | b为分支因子,d为解深度;实际远小于BFS |
| 空间 | O(d) | 只存储当前路径,线性空间 |
| 最优性 | 是 | 保证找到最短路径(因为启发式可采纳) |
| 完备性 | 是 | 如果解存在一定能找到 |
扩展应用
- 8数码/24数码/48数码:相同的IDA*框架,仅改变棋盘大小
- 魔方求解:Kociemba算法本质也是IDA*的扩展
- ** Sokoban(推箱子)**:结合宏观移动(macro moves)减少分支因子
- 路径规划:在网格地图中寻找最短路径
- 游戏AI:实时策略游戏中的单位移动和寻路
总结
IDA搜索算法是状态空间搜索的利器,它将A的最优性与深度优先搜索的内存效率完美结合。对于15数码这类NP-hard拼图问题,IDA配合精心设计的启发式函数(如曼哈顿距离+线性冲突)能够在极短时间内求解。理解IDA的核心思想——迭代加深与阈值控制——不仅对拼图问题有帮助,也是学习更高级搜索算法(如RBFS、SMA*)的重要基础。