Java 中的骑士巡游问题

2025年5月14日 | 阅读 4 分钟

骑士巡游问题是回溯算法的一个著名实例。它涉及到让一个骑士在棋盘上移动,以便它恰好访问每个格子一次。给定一个 (n 乘以 n) 的棋盘和一个起始位置,目标是移动骑士,使其覆盖所有格子而不重复。

这个问题可以通过回溯来解决,我们在其中测试每一种可能的移动,如果一种移动能够导向解决方案,就继续进行;否则,我们就回溯以尝试另一条路径。

问题陈述

国际象棋中的骑士以“L”形移动,在一个方向(垂直或水平)上移动两个格子,然后向初始移动的垂直方向移动一个格子。根据格子的位置,骑士最多可以进行八种移动。骑士巡游问题要求我们创建一个路径,能够遍历每个格子一次。这可以通过以下方式实现:

  1. 回溯:探索所有可能的移动,我们一次移动一步。如果一次移动能到达一个未被访问过的格子,我们就从那里继续。如果不能,我们就回溯并尝试下一个可能的移动。
  2. 沃恩斯多夫规则(优化):这种启发式方法通过选择导致后续移动最少的移动来最小化陷入死胡同的可能性。

使用回溯解决骑士巡游问题的 Java 代码

文件名:KnightsTour.java

输出

 
 0 59 38 33 30 17  8 63 
37 34 31 60  9 62 29 16 
58  1 36 39 32 27 18  7 
35 48 41 26 61 10 15 28 
42 57  2 49 40 23  6 19 
47 50 45 54 25 20 11 14 
56 43 52  3 22 13 24  5 
51 46 55 44 53  4 21 12    

解释

解决骑士巡游问题的 Java 代码通过初始化一个 N×N 的棋盘,其中每个单元格都设置为 -1,表示尚未被访问。骑士从左上角开始,其 8 种可能的移动定义在 moveX 和 moveY 数组中。solveKTUtil 函数使用递归和回溯来依次尝试每种移动。

如果一个移动是有效的(在边界内且未被访问),骑士会用当前的移动计数标记它,然后递归地尝试下一个移动。如果一个移动没有导向解决方案,骑士会通过将该单元格重置为 -1 来回溯,并尝试另一个移动。

这个过程会一直持续,直到骑士覆盖了所有格子(找到了解决方案),或者所有的选择都被耗尽(在这种情况下没有解决方案)。如果找到了解决方案,printSolution() 函数会显示巡游路径,显示每个格子被访问的顺序。

复杂度分析

该算法的时间复杂度为 (O(8^N * N)),因为每个格子都可能导致八次递归调用,从而导致指数增长。然而,剪掉超出边界或指向已访问格子的移动会显著减少实际运行时间。空间复杂度为 (O(N^2)),用于存储棋盘和递归调用栈。

结论

骑士巡游问题是一个经典的示例,说明了回溯问题,其中尝试每一步移动,如果导致死胡同则进行撤销。尽管理论上复杂度很高,但回溯对于这种有约束的解决方案是有效的。

虽然沃恩斯多夫规则等改进可以通过优先选择前向选择较少的移动来进一步优化性能,但仅使用递归回溯方法对于中等尺寸的棋盘来说通常已经足够,使其成为算法设计中一个高效且具有教育意义的问题解决示例。