
华为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在线答疑。

45万+

被折叠的 条评论
为什么被折叠?



