华为OD机试 - 单词搜索计数 - 深度优先搜索DFS(Java 新系统 200分)

在这里插入图片描述

华为OD机试 新系统 题库疯狂收录中,刷题点这里

专栏导读

本专栏收录于《华为OD机试(JAVA)真题》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天CSDN在线答疑。

一、题目描述

给定一个mxn的二维字符网格board和一个字符串🔗单词word

请计算单词word在网格中出现的总次数。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个搜索路径中不允许被重复使用。

二、输入描述

bord:二维字符列表,每个元素为大写英文字母,1≤m,n≤10。

word:字符串,由大写英文字母组成,1≤len(word)≤100。

三、输出描述

返回一个整数,表示单词在网格中出现的路径总数。

四、测试用例

测试用例1:

1、输入

ABCE SFCS ADEE
ABCCED

2、输出

1

3、说明

存在唯一的一条路径可以依次组成 ABCCED,因此答案为 1。

测试用例2:

1、输入

AB BA
ABA

2、输出

4

3、说明

网格为:

A B
B A

两个 A 均可以作为起点,每个起点都有两种合法的 A→B→A 路径,因此总数为:

2 × 2 = 4

五、解题思路

本题适合使用“深度优先搜索 DFS + 回溯”解决。首先枚举二维网格中的每一个单元格,当当前位置字符等于 word 的第一个字符时,将该位置作为一条候选路径的起点进行 DFS。DFS 每次只向上、下、左、右四个相邻单元格移动,并要求下一个位置的字符必须等于 word 中待匹配的下一个字符。当已经匹配到 word 的最后一个字符时,说明找到了一条完整合法路径,答案加一。

由于题目规定同一个单元格在同一条搜索路径中不能重复使用,因此搜索进入某个格子后,将该格子临时修改为特殊字符 ‘#’,这样后续递归不会再次访问它;递归结束后再恢复原字符,这就是标准的回溯思想。这样不需要额外创建 visited 二维数组,可以节省额外空间。

在 DFS 前加入两个剪枝。第一,如果 word 的长度大于 m×n,由于每个格子最多使用一次,可以直接返回 0。第二,统计网格和 word 中 26 个大写字母的出现次数,只要 word 中某个字符所需数量超过网格中的数量,也可以立即返回 0。

另外比较 word 首尾字符在网格中的出现次数,如果尾字符更少,则将 word 反转后再搜索。任意一条正向合法路径反向后都对应一条反向合法路径,因此反转不会改变答案,却可以从更稀有的字符开始搜索,从而减少 DFS 起点和无效分支。

主要数据结构为 char[][] 二维数组、长度为 26 的字符频次数组以及递归调用栈。最坏时间复杂度约为 O(m×n×3^(L-1)),L 为 word 长度;递归空间复杂度为 O(L)。

六、Java算法源码

public class OdTest {

    private static int rows;
    private static int cols;
    private static char[][] board;
    private static char[] target;

    // 使用 BigInteger,避免路径数量较大时发生整数溢出
    private static BigInteger total = BigInteger.ZERO;

    // 上、下、左、右四个方向
    private static final int[][] DIRS = {
            {1, 0},
            {-1, 0},
            {0, 1},
            {0, -1}
    };

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        if (!scanner.hasNextLine()) {
            return;
        }

        String boardLine = scanner.nextLine().trim();

        if (boardLine.isEmpty() || !scanner.hasNextLine()) {
            System.out.println(0);
            return;
        }

        String word = scanner.nextLine().trim();

        /*
         * 根据题目样例:
         * ABCE SFCS ADEE
         *
         * 表示三行:
         * ABCE
         * SFCS
         * ADEE
         */
        String[] rowStrings = boardLine.split("\\s+");

        rows = rowStrings.length;
        cols = rowStrings[0].length();

        board = new char[rows][cols];

        // 构造二维字符数组,同时检查每一行长度是否一致
        for (int i = 0; i < rows; i++) {
            if (rowStrings[i].length() != cols) {
                System.out.println(0);
                return;
            }

            board[i] = rowStrings[i].toCharArray();
        }

        System.out.println(countOccurrences(word));
    }

    /**
     * 统计目标单词在字符网格中的合法路径总数
     */
    private static BigInteger countOccurrences(String word) {

        if (word == null || word.isEmpty() || rows == 0 || cols == 0) {
            return BigInteger.ZERO;
        }

        /*
         * 每个格子在一条路径中最多使用一次。
         * 如果单词长度超过网格格子总数,则一定不可能匹配。
         */
        if (word.length() > rows * cols) {
            return BigInteger.ZERO;
        }

        int[] boardFreq = new int[26];

        for (char[] row : board) {
            for (char c : row) {
                boardFreq[c - 'A']++;
            }
        }

        int[] wordFreq = new int[26];

        for (char c : word.toCharArray()) {
            wordFreq[c - 'A']++;
        }

        /*
         * 频次剪枝:
         * 如果目标单词需要的某个字符数量比整个网格中该字符的数量还多,
         * 则无论如何都无法组成目标单词。
         */
        for (int i = 0; i < 26; i++) {
            if (wordFreq[i] > boardFreq[i]) {
                return BigInteger.ZERO;
            }
        }

        /*
         * 搜索方向优化:
         * 如果 word 最后一个字符在网格中比第一个字符更少,
         * 就将 word 反转,从更加稀有的字符开始 DFS。
         *
         * 例如原路径 A -> B -> C,
         * 反向后就是 C -> B -> A,
         * 两者是一一对应关系,因此不会改变最终路径数量。
         */
        if (boardFreq[word.charAt(word.length() - 1) - 'A']
                < boardFreq[word.charAt(0) - 'A']) {

            word = new StringBuilder(word)
                    .reverse()
                    .toString();
        }

        target = word.toCharArray();
        total = BigInteger.ZERO;

        /*
         * 枚举网格中的每一个位置。
         * 只有当前位置等于单词首字符时才启动 DFS。
         */
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {

                if (board[r][c] == target[0]) {
                    dfs(r, c, 0);
                }
            }
        }

        return total;
    }

    /**
     * 从 board[r][c] 开始尝试匹配 target[index]
     */
    private static void dfs(int r, int c, int index) {

        if (board[r][c] != target[index]) {
            return;
        }

        // 已经成功匹配到单词最后一个字符,找到一条完整路径
        if (index == target.length - 1) {
            total = total.add(BigInteger.ONE);
            return;
        }

        /*
         * 同一个单元格在同一条搜索路径中不能重复使用。
         *
         * 因为原网格只包含大写英文字母,
         * 所以可以临时使用 '#' 标记当前格子已访问。
         */
        char original = board[r][c];
        board[r][c] = '#';

        // 向四个相邻方向继续寻找下一个字符
        for (int[] dir : DIRS) {

            int nextRow = r + dir[0];
            int nextCol = c + dir[1];

            if (nextRow >= 0
                    && nextRow < rows
                    && nextCol >= 0
                    && nextCol < cols
                    && board[nextRow][nextCol] == target[index + 1]) {

                dfs(nextRow, nextCol, index + 1);
            }
        }

        /*
         * 回溯:
         * 恢复当前格子的原始字符,
         * 这样其他搜索路径仍然可以使用该格子。
         */
        board[r][c] = original;
    }
}

七、效果展示

1、输入

ABCD EFGH IJKL
AEI

2、输出

1

3、说明

因此只有 1 条合法路径。在这里插入图片描述


🏆下一篇:华为OD机试 - 简易内存池 - 逻辑分析(Java 新系统 200分)

🏆本专栏收录于《华为OD机试(JAVA)真题》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天CSDN在线答疑。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哪 吒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值