《国际象棋跳跃问题》、《小A的移动点》 | 豆包MarsCode AI刷题

201 阅读4分钟

一、国际象棋跳跃问题

问题理解

小U掌握了国际象棋中“象”和“马”的跳跃能力,在一个无限大的平面直角坐标系中,她每一步可以模仿象和马的跳跃方式移动。在每次询问中,小U需要计算从初始坐标 (x1, y1) 到目标坐标 (x2, y2) 所需的最少步数。

  • 象的跳跃:可以移动到 (x + k, y + k) 或 (x + k, y - k),其中 k 是任意整数。
  • 马的跳跃:可以移动到 (x + 2, y + 1)(x + 2, y - 1)(x - 2, y + 1)(x - 2, y - 1)(x + 1, y + 2)(x + 1, y - 2)(x - 1, y + 2)(x - 1, y - 2)

数据结构选择

  • 队列:用于广度优先搜索(BFS),记录当前坐标和步数。
  • 集合:用于记录已访问的坐标,防止重复访问。

算法步骤

  1. 初始化

    • 创建一个队列 queue,初始状态为 (x1, y1, 0),表示起点坐标和步数为 0
    • 创建一个集合 visited,记录已访问的坐标,初始状态为 {(x1, y1)}
  2. 广度优先搜索(BFS)

    • 当队列不为空时,执行以下操作:

      • 从队列中取出当前坐标 (x, y) 和步数 steps

      • 处理马的跳跃:

        • 对于每个马的跳跃方向 (dx, dy),计算新坐标 (nx, ny)
        • 如果新坐标等于目标坐标 (x2, y2),返回 steps + 1
        • 如果新坐标未被访问过,加入队列并标记为已访问。
      • 处理象的跳跃:

        • 对于每个可能的 k 值,计算新坐标 (nx1, ny1) 和 (nx2, ny2)
        • 如果新坐标等于目标坐标 (x2, y2),返回 steps + 1
        • 如果新坐标未被访问过,加入队列并标记为已访问。
  3. 返回结果

    • 如果队列为空且未找到目标坐标,返回 -1(理论上不会到达这里,因为题目保证会有解)。

代码实现

from collections import deque

def solution(x1, y1, x2, y2):

    # 马的跳跃方向
    knight_moves = [(21), (2, -1), (-21), (-2, -1), (12), (1, -2), (-12), (-1, -2)]

    # 初始化队列和已访问集合
    queue = deque([(x1, y1, 0)])
    visited = set()
    visited.add((x1, y1))

    while queue:
        x, y, steps = queue.popleft()
        # 先处理马的跳跃
        for dx, dy in knight_moves:
            nx, ny = x + dx, y + dy
            if (nx, ny) == (x2, y2):
                return steps + 1
            if (nx, ny) not in visited:
                visited.add((nx, ny))
                queue.append((nx, ny, steps + 1))

        # 然后处理象的跳跃
        # 只需考虑在当前方向上加减 k
        for k in range(-100101):  # 选择一个合理的范围
            # 对角线移动
            nx1, ny1 = x + k, y + k
            if (nx1, ny1) == (x2, y2):
                return steps + 1
            if (nx1, ny1) not in visited:
                visited.add((nx1, ny1))
                queue.append((nx1, ny1, steps + 1))

            nx2, ny2 = x + k, y - k
            if (nx2, ny2) == (x2, y2):
                return steps + 1
            if (nx2, ny2) not in visited:
                visited.add((nx2, ny2))
                queue.append((nx2, ny2, steps + 1))

    return -1  # 理论上不会到达这里,因为题目保证会有解

二、小A的移动点

问题理解

小M有 n 个点,每个点的坐标为 (xi, yi)。她可以从一个点出发,平行于坐标轴移动,直到到达另一个点。具体来说,她可以从 (x1, y1) 移动到 (x2, y1) 或者 (x1, y2)。你的任务是计算最少需要增加多少个点,才能保证任意两个点之间可以通过平行于坐标轴的路径互相到达。

数据结构选择

  • 并查集(Union-Find) :用于管理和合并连通分量。
  • 字典(defaultdict) :用于按 x 和 y 轴分组点的索引。

算法步骤

  1. 初始化并查集

    • 创建一个并查集 parent,初始时每个点的父节点是它自己。
    • find 函数用于查找并返回节点 x 的根节点,并进行路径压缩。
    • union 函数用于合并两个节点 x 和 y 所在的连通分量。
  2. 按 x 和 y 轴分组

    • 使用两个字典 x_groups 和 y_groups,分别按 x 和 y 轴分组点的索引。
  3. 合并同一组的点

    • 对于每个 x 轴分组中的点,使用并查集将它们合并。
    • 对于每个 y 轴分组中的点,使用并查集将它们合并。
  4. 计算连通分量数量

    • 遍历所有点,使用并查集找到每个点的根节点,统计不同根节点的数量。
  5. 计算最少新增点数

    • 最少新增点数为连通分量数量减 1

代码实现

from collections import defaultdict

def solution(n: int, points: list) -> int:

    # 初始化并查集
    parent = list(range(n))

    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]

    def union(x, y):
        rootX = find(x)
        rootY = find(y)
        if rootX != rootY:
            parent[rootY] = rootX

    # 按 x 和 y 轴分组
    x_groups = defaultdict(list)
    y_groups = defaultdict(list)

    for i, (x, y) in enumerate(points):
        x_groups[x].append(i)
        y_groups[y].append(i)

    # 合并同一组的点
    for indices in x_groups.values():
        for i in range(1len(indices)):
            union(indices[0], indices[i])

    for indices in y_groups.values():
        for i in range(1len(indices)):
            union(indices[0], indices[i])

    # 计算连通分量数量
    unique_roots = len(set(find(i) for i in range(n)))

    # 最少新增点数为连通分量数量减 1
    return unique_roots - 1