文章

[LeetCode] 每日一题 2056. 棋盘上有效移动组合的数目

题目链接

https://leetcode.cn/problems/number-of-valid-move-combinations-on-chessboard

题目描述

有一个 8 x 8 的棋盘,它包含 n 个棋子(棋子包括车,后和象三种)。给你一个长度为 n 的字符串数组 pieces ,其中 pieces[i] 表示第 i 个棋子的类型(车,后或象)。除此以外,还给你一个长度为 n 的二维整数数组 positions ,其中 positions[i] = [ri, ci] 表示第 i 个棋子现在在棋盘上的位置为 (ri, ci) ,棋盘下标从 1 开始。

棋盘上每个棋子都可以移动 至多一次 。每个棋子的移动中,首先选择移动的 方向 ,然后选择 移动的步数 ,同时你要确保移动过程中棋子不能移到棋盘以外的地方。棋子需按照以下规则移动:

  • 车可以 水平或者竖直 从 (r, c) 沿着方向 (r+1, c)(r-1, c)(r, c+1) 或者 (r, c-1) 移动。

  • 后可以 水平竖直或者斜对角 从 (r, c) 沿着方向 (r+1, c)(r-1, c)(r, c+1)(r, c-1)(r+1, c+1)(r+1, c-1)(r-1, c+1)(r-1, c-1) 移动。

  • 象可以 斜对角 从 (r, c) 沿着方向 (r+1, c+1)(r+1, c-1)(r-1, c+1)(r-1, c-1) 移动。

移动组合 包含所有棋子的 移动 。每一秒,每个棋子都沿着它们选择的方向往前移动 一步 ,直到它们到达目标位置。所有棋子从时刻 0 开始移动。如果在某个时刻,两个或者更多棋子占据了同一个格子,那么这个移动组合 不有效 。

请你返回 有效 移动组合的数目。

注意:

  • 初始时,不会有两个棋子 在 同一个位置 。

  • 有可能在一个移动组合中,有棋子不移动。

  • 如果两个棋子 直接相邻 且两个棋子下一秒要互相占据对方的位置,可以将它们在同一秒内 交换位置 。

示例输入

示例 1

输入:pieces = ["rook"], positions = [[1,1]]

输出:15

解释:上图展示了棋子所有可能的移动。

示例 2

输入:pieces = ["queen"], positions = [[1,1]]

输出:22

解释:上图展示了棋子所有可能的移动。

示例 3

输入:pieces = ["bishop"], positions = [[4,3]]

输出:12

解释:上图展示了棋子所有可能的移动。

示例 4

输入:pieces = ["rook","rook"], positions = [[1,1],[8,8]]

输出:223

解释:每个车有 15 种移动,所以总共有 15 * 15 = 225 种移动组合。
但是,有两个是不有效的移动组合:
- 将两个车都移动到 (8, 1) ,会导致它们在同一个格子相遇。
- 将两个车都移动到 (1, 8) ,会导致它们在同一个格子相遇。
所以,总共有 225 - 2 = 223 种有效移动组合。
注意,有两种有效的移动组合,分别是一个车在 (1, 8) ,另一个车在 (8, 1) 。
即使棋盘状态是相同的,这两个移动组合被视为不同的,因为每个棋子移动操作是不相同的。

示例 5

输入:pieces = ["queen","bishop"], positions = [[5,7],[3,4]]

输出:281

解释:总共有 12 * 24 = 288 种移动组合。
但是,有一些不有效的移动组合:
- 如果后停在 (6, 7) ,它会阻挡象到达 (6, 7) 或者 (7, 8) 。
- 如果后停在 (5, 6) ,它会阻挡象到达 (5, 6) ,(6, 7) 或者 (7, 8) 。
- 如果象停在 (5, 2) ,它会阻挡后到达 (5, 2) 或者 (5, 1) 。
在 288 个移动组合当中,281 个是有效的。

提示

  • n == pieces.length

  • n == positions.length

  • 1 <= n <= 4

  • pieces 只包含字符串 "rook" ,"queen" 和 "bishop" 。

  • 棋盘上最多只有一个后。

  • 1 <= ri, ci <= 8

  • 每一个 positions[i] 互不相同。

题解

题目理解

我们在一个 8x8 的棋盘上,有若干个棋子。每个棋子有一个初始位置,可以沿着特定的方向进行多步合法移动。要求计算这些棋子在给定的初始位置和移动规则下,所有合法的移动组合的数量。合法的移动组合是指这些棋子的移动过程中不会发生重叠,即不会有任何两个棋子在同一时刻处于相同的格子。

核心思路

这个问题的核心在于回溯(backtracking)和剪枝。我们可以通过以下几个步骤来解决问题:

  1. 预处理每个棋子的所有合法移动

    • 每个棋子的合法移动受限于其初始位置、可用的移动方向以及步数。我们首先根据每个棋子的类型(车、象、皇后等)和其初始位置,预先计算出该棋子在所有方向上可能的合法移动路径。

  2. 回溯暴力枚举所有合法的移动组合

    • 对于每个棋子,我们枚举它所有的合法移动路径。如果某条路径与前面已选择的棋子发生冲突(即棋子在相同的时刻重叠),则不继续选择该路径。

    • 如果当前棋子的所有路径都没有冲突,我们就递归到下一个棋子,直到所有棋子的路径都被选定。

  3. 剪枝优化

    • 在回溯过程中,如果当前棋子的路径与之前棋子发生了冲突,立即跳过该路径。这样可以避免不必要的递归,提升效率。

具体实现

  1. 预处理合法路径

    • 对于每个棋子,生成其可能的移动路径,包括原地不动和沿着每个方向的多步移动。

  2. 路径冲突判断

    • 定义一个crashed方法,判断两个棋子是否在相同的时间步内重叠。具体地,我们通过模拟每个棋子按步数移动来判断是否发生重叠。

  3. 回溯算法

    • 使用回溯算法来暴力枚举每个棋子的所有合法路径。如果某个路径与之前的路径冲突,则剪枝,跳过当前路径。

class Solution {
    // 每种棋子的移动方向
    static Map<String, int[][]> piecesDir = Map.of(
        "rook", new int[][] { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } },
        "bishop", new int[][] { { 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 } },
        "queen", new int[][] { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 }, { 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 } }
    );

    public int countCombinations(String[] pieces, int[][] positions) {
        int n = pieces.length;
        List<List<Path>> allPaths = new ArrayList<>();

        // 为每个棋子生成所有的合法路径
        for (int i = 0; i < n; i++) {
            allPaths.add(genPath(piecesDir.get(pieces[i]), positions[i][0], positions[i][1]));
        }

        Path[] move = new Path[n]; // 当前棋子的移动路径
        return dfs(0, n, allPaths, move);
    }

    // 生成每个棋子在特定方向上的所有合法路径
    private List<Path> genPath(int[][] dirs, int x, int y) {
        List<Path> paths = new ArrayList<>();
        // 原地不动
        paths.add(new Path(x, y, 0, 0, 0));
        for (int[] dir : dirs) {
            int px = x + dir[0];
            int py = y + dir[1];
            for (int step = 1; px > 0 && py > 0 && px <= 8 && py <= 8; step++) {
                paths.add(new Path(x, y, dir[0], dir[1], step));
                px += dir[0];
                py += dir[1];
            }
        }
        return paths;
    }

    private int dfs(int i, int n, List<List<Path>> allPaths, Path[] move) {
        if (i == n) {
            return 1; // 所有棋子都有合法路径
        }
        int res = 0;
        // 枚举当前棋子的所有合法路径
        for (Path path : allPaths.get(i)) {
            boolean skip = false;
            for (int j = 0; j < i; j++) {
                if (path.crashed(move[j])) {
                    skip = true; // 路径冲突,跳过
                    break; // 一旦发现冲突,跳出内层循环
                }
            }
            if (skip) {
                continue; // 跳过当前路径,继续检查下一个路径
            }
            move[i] = path; // 记录当前棋子的路径
            res += dfs(i + 1, n, allPaths, move); // 递归检查下一个棋子的路径
        }
        return res;
    }

    // 定义棋子的路径
    static class Path {
        int x;
        int y;
        int dx;
        int dy;
        int step;

        Path(int x, int y, int dx, int dy, int step) {
            this.x = x;
            this.y = y;
            this.dx = dx;
            this.dy = dy;
            this.step = step;
        }

        // 判断是否与其它路径相冲突
        boolean crashed(Path other) {
            int x1 = x, y1 = y;
            int x2 = other.x, y2 = other.y;
            for (int i = 0; i < Math.max(step, other.step); i++) {
                // 模拟棋子移动
                if (i < step) {
                    x1 += dx;
                    y1 += dy;
                }
                if (i < other.step) {
                    x2 += other.dx;
                    y2 += other.dy;
                }
                if (x1 == x2 && y1 == y2) {
                    return true;
                }
            }
            return false;
        }
    }
}

希望这篇分享能为你带来启发!如果你有任何问题或建议,欢迎在评论区留言,与我共同交流探讨。

License:  CC BY 4.0