UVa 633 A Chess Knight

题目描述

在一个 2N×2N2N \times 2N2N×2N 的棋盘上(3≤N≤203 \le N \le 203N20),有一只“动态骑士”。它可以执行三种类型的移动:

  • K\texttt{K}K 型:与常规国际象棋骑士相同,即沿水平或垂直方向移动 222 格,再沿垂直方向移动 111 格,共有 888 个可能的方向。
  • B\texttt{B}B 型:沿对角线方向移动 222 格,共有 444 个可能的方向。
  • T\texttt{T}T 型:沿棋盘的水平中轴或垂直中轴进行镜像反射,即坐标 (x,y)(x, y)(x,y) 变为 (2N+1−x,y)(2N+1-x, y)(2N+1x,y)(x,2N+1−y)(x, 2N+1-y)(x,2N+1y)

骑士不能走出棋盘,也不能落在障碍物上。此外,骑士不能连续两次使用相同类型的移动(第一次移动可以任意类型)。给定起点和终点,求从起点到终点的最少移动次数。如果无法到达,则报告无解。

输入格式

输入包含多组测试用例。每个测试用例的第一行为一个整数 NNNN>0N > 0N>0),棋盘大小为 2N×2N2N \times 2N2N×2N。第二行为起点坐标 (xs,ys)(x_s, y_s)(xs,ys),第三行为终点坐标 (xt,yt)(x_t, y_t)(xt,yt)。随后若干行给出障碍物的坐标,每行两个整数,以 0 0 结束障碍物描述。输入以单独的 N=0N = 0N=0 结束。

输出格式

对于每个测试用例:

  • 若起点与终点相同,输出 Result : 0
  • 若终点是障碍物,输出 Solution doesn't exist
  • 否则,输出最少移动次数,格式为 Result : X,其中 XXX 为步数;若不可达,则输出 Solution doesn't exist

样例

输入

3
1 1
1 1
2 2
0 0
10
1 1
20 20
20 1
1 20
3 1
3 2
3 3
2 3
1 3
0 0
10
2 1
18 12
2 2
5 6
7 2
8 3
9 4
1 15
7 12
8 13
9 11
11 9
12 4
11 3
9 5
2 7
3 8
6 5
0 0
3
1 1
5 4
2 2
2 4
2 5
3 1
3 2
3 3
3 6
4 2
4 3
4 5
5 1
5 3
5 5
6 1
6 4
6 5
6 6
0 0

输出

Result : 0
Solution doesn't exist
Result : 6
Result : 4

题目分析

本题要求在带障碍物的棋盘上求最短路径,但移动规则比常规骑士复杂,且包含三种不同类型的移动,并限制不能连续使用同类型。由于移动类型只有三种,且每种类型的移动数量有限(K\texttt{K}K888 种,B\texttt{B}B444 种,T\texttt{T}T222 种),棋盘最大尺寸为 40×4040 \times 4040×40,状态总数可控。

最短路问题可以用广度优先搜索(BFS\texttt{BFS}BFS)求解,但状态不仅包含坐标 (x,y)(x, y)(x,y),还需要记录上一步使用的移动类型,因为规则限制了连续相同类型。因此每个位置对应 444 个状态(上一步为无、K\texttt{K}KB\texttt{B}BT\texttt{T}T),分别标记访问情况。BFS\texttt{BFS}BFS 从起点开始,每次扩展所有合法移动,并确保新移动的类型与上一步不同。当扩展到终点时,当前步数即为最少步数。

解题思路

状态表示与访问标记

定义结构体 movement 包含坐标 xy、当前步数 steps 以及上一步移动类型 lastMoveType000 表示起点,无上一步;111 表示 K\texttt{K}K222 表示 B\texttt{B}B333 表示 T\texttt{T}T)。使用三维数组 visited[x][y][type] 记录是否已访问过该状态,其中 type 对应上一步的类型(000333)。若 visited[x][y][type] 为真,则不再入队,因为 BFS\texttt{BFS}BFS 首次访问即最优。

移动生成

  • K\texttt{K}K 型:枚举 888 个偏移量 (Δx,Δy)∈{(−2,−1),(−1,−2),(1,−2),(2,−1),(2,1),(1,2),(−1,2),(−2,1)}(\Delta x, \Delta y) \in \{(-2,-1),(-1,-2),(1,-2),(2,-1),(2,1),(1,2),(-1,2),(-2,1)\}(Δx,Δy){(2,1),(1,2),(1,2),(2,1),(2,1),(1,2),(1,2),(2,1)},检查目标坐标是否在棋盘内且非障碍物。
  • B\texttt{B}B 型:枚举 444 个偏移量 (Δx,Δy)∈{(−2,−2),(2,−2),(2,2),(−2,2)}(\Delta x, \Delta y) \in \{(-2,-2),(2,-2),(2,2),(-2,2)\}(Δx,Δy){(2,2),(2,2),(2,2),(2,2)},同样检查合法性。
  • T\texttt{T}T 型:计算镜像坐标 (2N+1−x,y)(2N+1-x, y)(2N+1x,y)(x,2N+1−y)(x, 2N+1-y)(x,2N+1y),若目标非障碍物则入队。

搜索流程

  1. 读入 NNN,若 N=0N=0N=0 则结束。
  2. 读入起点、终点,并读入障碍物坐标直到 0 0
  3. 若起点与终点重合,输出 Result : 0 并继续下一组。
  4. 若终点是障碍物,输出 Solution doesn't exist 并继续。
  5. 初始化队列,将起点状态(lastMoveType = 0,步数为 000)入队,并标记 visited[startx][starty][0] = 1
  6. 执行 BFS\texttt{BFS}BFS
    • 取出队首状态。
    • 若坐标等于终点,输出当前步数并标记找到。
    • 否则,对于三种移动类型(若当前上一步类型不等于该类型),尝试所有该类型的合法移动,若目标状态未访问,则入队并标记。
  7. 若队列空仍未找到,输出 Solution doesn't exist

边界与剪枝

  • 棋盘大小最大 404040,状态数为 40×40×4=640040 \times 40 \times 4 = 640040×40×4=6400,非常小。
  • 注意 T 型移动可能回到自身(当 x=N+0.5x = N+0.5x=N+0.5 时?但 2N2N2N 为偶数,镜像坐标不可能与自身重合,因为 (2N+1−x)=x(2N+1-x) = x(2N+1x)=x 得到 x=N+0.5x = N+0.5x=N+0.5,不是整数,所以不会自环)。
  • 障碍物只禁止停留,不禁止经过(移动是跳跃式的,不经过中间格)。

复杂度分析

  • 每个状态最多扩展一次,状态数为 O((2N)2×4)=O(N2)O((2N)^2 \times 4) = O(N^2)O((2N)2×4)=O(N2)N≤20N \le 20N20,故状态数不超过 640064006400
  • 每次扩展尝试最多 8+4+2=148+4+2 = 148+4+2=14 种移动,总扩展次数 O(N2×14)O(N^2 \times 14)O(N2×14),非常小。
  • 空间复杂度 O(N2)O(N^2)O(N2)

代码实现

// A Chess Knight
// UVa ID: 633
// Verdict: Accepted
// Submission Date: 2017-06-02
// UVa Run Time: 0.000s
//
// 版权所有(C)2017,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

struct movement {
    int x, y, steps, lastMoveType;
};

int main() {
    cin.tie(0), cout.tie(0), ios::sync_with_stdio(false);
    int obstacle[64][64], visited[64][64][4], n;
    int offsetK[8][2] = {
        {-2, -1}, {-1, -2}, {1, -2}, {2, -1},
        {2, 1}, {1, 2}, {-1, 2}, {-2, 1}
    };
    int offsetB[4][2] = {
        {-2, -2}, {2, -2}, {2, 2}, {-2, 2}
    };
    while (cin >> n, n > 0) {
        n *= 2;
        memset(obstacle, 0, sizeof(obstacle));
        memset(visited, 0, sizeof(visited));
        int startx, starty, endx, endy;
        cin >> startx >> starty >> endx >> endy;
        int obstaclex, obstacley;
        while (cin >> obstaclex >> obstacley) {
            if (obstaclex == 0 && obstacley == 0) break;
            obstacle[obstaclex][obstacley] = 1;
        }
        if (startx == endx && starty == endy) {
            cout << "Result : 0\n";
            continue;
        }
        if (obstacle[endx][endy]) {
            cout << "Solution doesn't exist\n";
            continue;
        }
        queue<movement> unvisited;
        unvisited.push(movement{startx, starty, 0, 0});
        bool found = false;
        while (!unvisited.empty()) {
            movement m = unvisited.front();
            unvisited.pop();
            if (m.x == endx && m.y == endy) {
                cout << "Result : " << m.steps << '\n';
                found = true;
                break;
            }
            if (m.lastMoveType != 1 && !visited[m.x][m.y][1]) {
                for (int i = 0; i < 8; i++) {
                    int nextx = m.x + offsetK[i][0], nexty = m.y + offsetK[i][1];
                    if (nextx < 1 || nextx > n || nexty < 1 || nexty > n) continue;
                    if (obstacle[nextx][nexty]) continue;
                    unvisited.push(movement{nextx, nexty, m.steps + 1, 1});
                }
                visited[m.x][m.y][1] = 1;
            }
            if (m.lastMoveType != 2 && !visited[m.x][m.y][2]) {
                for (int i = 0; i < 4; i++) {
                    int nextx = m.x + offsetB[i][0], nexty = m.y + offsetB[i][1];
                    if (nextx < 1 || nextx > n || nexty < 1 || nexty > n) continue;
                    if (obstacle[nextx][nexty]) continue;
                    unvisited.push(movement{nextx, nexty, m.steps + 1, 2});
                }
                visited[m.x][m.y][2] = 1;
            }
            if (m.lastMoveType != 3 && !visited[m.x][m.y][3]) {
                int nextx = n - m.x + 1, nexty = m.y;
                if (!obstacle[nextx][nexty])
                    unvisited.push(movement{nextx, nexty, m.steps + 1, 3});
                nextx = m.x, nexty = n - m.y + 1;
                if (!obstacle[nextx][nexty])
                    unvisited.push(movement{nextx, nexty, m.steps + 1, 3});
                visited[m.x][m.y][3] = 1;
            }
        }
        if (!found)  cout << "Solution doesn't exist\n";
    }
    return 0;
}

总结

本题通过引入移动类型和不能连续使用同类型的限制,将普通的最短路径问题扩展为带状态的最短路问题。利用 BFS\texttt{BFS}BFS 在状态空间(坐标 + 上一步类型)上进行搜索,能够保证首次到达终点时即为最少步数。关键点包括:

  • 状态设计必须包含上一步移动类型,以正确应用限制规则。
  • 特殊处理起点与终点重合以及终点为障碍物的情况。
  • 移动生成时注意边界检查和障碍物检查。
  • 由于棋盘较小,状态总数有限,BFS\texttt{BFS}BFS 效率很高。

此解法清晰直观,体现了在复杂移动规则下使用 BFS\texttt{BFS}BFS 求解最短路径的通用思路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值