这道题目是一个经典的骑士巡逻问题(Knight’s Tour Problem)的变体。它的核心要求是:在给定的
题目的难点在于 字典序, 你自己画个图就知道了
1. 核心难点解析
-
字典序最小(Lexicographically Smallest):
这是本题的关键。棋盘的坐标表示为“字母+数字”(如 A1, B2)。字典序比较时,先比字母,再比数字。
- 起点选择:为了让路径字典序最小,必须从 A1 开始。因为任何从 B1 或 A2 开始的路径,其首字符在字典序上都大于 A1。
- 移动顺序:在 DFS(深度优先搜索)过程中,骑士有 8 个可选方向。为了保证找到的第一条完整路径就是字典序最小的,我们需要按照特定的顺序尝试这 8 个方向。
2. 贪心策略:确定 8 个方向的顺序
假设当前坐标为
(例如:从 C3 移动到 A2)
严格遵守这个顺序进行 DFS,找到的第一组解即为最优解。
3. 算法步骤 (DFS + 回溯)
- 初始化:
- 读取测试用例数量
。 - 对于每个用例,创建一个
的标记数组 visited,记录方格是否走过。 - 设定总步数目标为
。
- 读取测试用例数量
- 搜索 (DFS):
- 从
A1(即坐标(1, 1)) 开始搜索。 - 标记当前格为已访问,并存入路径序列。
- 递归基准:如果路径长度等于
,说明已遍历全图,返回成功。 - 尝试移动:按照上述 8 个方向的顺序依次尝试。
- 检查合法性:目标格必须在棋盘内且未被访问。
- 回溯:如果某个分支走不通,撤销标记,尝试下一个方向。
- 从
- 输出:
- 如果 DFS 找到解,输出路径。
- 如果 DFS 遍历完所有可能仍未找到解,输出
impossible。
4. 逻辑实现伪代码
cpp
// 8个方向的偏移量 (注意顺序:先看col位移,再看row位移)
int dx[] = {-2, -2, -1, -1, 1, 1, 2, 2};
int dy[] = {-1, 1, -2, 2, -2, 2, -1, 1};
bool dfs(int x, int y, int step) {
if (step == p * q) return true; // 找齐了
for (int i = 0; i < 8; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx >= 1 && nx <= q && ny >= 1 && ny <= p && !vis[ny][nx]) {
vis[ny][nx] = true;
path[step] = {nx, ny}; // 记录路径
if (dfs(nx, ny, step + 1)) return true;
vis[ny][nx] = false; // 回溯
}
}
return false;
}
5. 注意事项
- 行列混淆:题目中
是数字(行), 是字母(列)。通常我们习惯 ,这里建议明确 对应列(A, B, C…), 对应行(1, 2, 3…)。 - 棋盘大小:
是个很小的范围,这意味着简单的 DFS 不会超时,不需要使用复杂的优化算法(如 Warnsdorff’s rule)。 - 不可能的情况:某些尺寸的棋盘(如
, 等)是无法完成全巡逻的,DFS 会自然返回失败。
代码
cpp
#include <iostream>
#include <cstring>
using namespace std;
// 全局变量定义
int P, Q; // P 为行数(1..p), Q 为列数(A..q)
bool visited[30][30]; // 标记是否访问过
struct Node {
int x, y; // 存储路径坐标
} path[30];
// 核心:方向数组必须按照字典序排列
// 规则:优先尝试字母小的(dx小),字母相同时尝试数字小的(dy小)
// 字母偏移 dx: -2, -2, -1, -1, 1, 1, 2, 2
// 数字偏移 dy: -1, 1, -2, 2, -2, 2, -1, 1
int dx[] = {-2, -2, -1, -1, 1, 1, 2, 2};
int dy[] = {-1, 1, -2, 2, -2, 2, -1, 1};
bool finished = false;
/**
* DFS 搜索函数
* @param currX 当前列坐标 (对应字母)
* @param currY 当前行坐标 (对应数字)
* @param step 当前步数
*/
void dfs(int currX, int currY, int step) {
path[step].x = currX;
path[step].y = currY;
if (step == P * Q) {
finished = true;
return;
}
for (int i = 0; i < 8; ++i) {
int nextX = currX + dx[i];
int nextY = currY + dy[i];
// 检查边界及访问情况
if (nextX >= 1 && nextX <= Q && nextY >= 1 && nextY <= P && !visited[nextY][nextX] && !finished) {
visited[nextY][nextX] = true;
dfs(nextX, nextY, step + 1);
// 回溯
if (!finished) {
visited[nextY][nextX] = false;
}
}
}
}
void solve(int caseNum) {
cin >> P >> Q;
// 初始化
memset(visited, false, sizeof(visited));
finished = false;
// 字典序最小路径必然从 A1 (1, 1) 开始
visited[1][1] = true;
dfs(1, 1, 1);
cout << "Scenario #" << caseNum << ":" << endl;
if (finished) {
for (int i = 1; i <= P * Q; ++i) {
// 将数字坐标转回字母和数字形式
// path[i].x 为列(字母), path[i].y 为行(数字)
char col = (char)(path[i].x + 'A' - 1);
cout << col << path[i].y;
}
cout << endl;
} else {
cout << "impossible" << endl;
}
cout << endl; // 每个测试用例后跟一个空行
}
int main() {
int n;
if (!(cin >> n)) return 0;
for (int i = 1; i <= n; ++i) {
solve(i);
}
return 0;
}
python
import sys
def solve():
# 读取输入
try:
line = sys.stdin.read().split()
if not line: return
n = int(line[0])
cases = []
for i in range(n):
cases.append((int(line[i*2+1]), int(line[i*2+2])))
except EOFError:
return
# 定义 8 个方向:严格按字典序(列增量优先,行增量次之)
# dx: 列(字母)位移, dy: 行(数字)位移
directions = [(-2, -1), (-2, 1), (-1, -2), (-1, 2),
(1, -2), (1, 2), (2, -1), (2, 1)]
for i, (P, Q) in enumerate(cases, 1):
visited = [[False] * Q for _ in range(P)]
path = []
def dfs(r, c):
path.append(f"{chr(ord('A') + c)}{r + 1}")
visited[r][c] = True
if len(path) == P * Q:
return True
for dc, dr in directions:
nc, nr = c + dc, r + dr
if 0 <= nr < P and 0 <= nc < Q and not visited[nr][nc]:
if dfs(nr, nc):
return True
# 回溯
visited[r][c] = False
path.pop()
return False
print(f"Scenario #{i}:")
# 始终从 A1 (0, 0) 开始以保证字典序最小
if dfs(0, 0):
print("".join(path))
else:
print("impossible")
print() # 每个 Case 后面的空行
if __name__ == "__main__":
solve()