【发布时间】:2020-02-22 22:53:34
【问题描述】:
这是给不熟悉这个问题的人的问题陈述:
给定一个 2D 棋盘和一个单词,找出该单词是否存在于网格中。 单词可以由顺序相邻单元格的字母构成,其中“相邻”单元格是水平或垂直相邻的单元格。同一个字母单元格不能多次使用。
我选择用 Java 来解决这个问题。在这两种解决方案中,主查询都需要一个函数exist(char[][] board, String word),我使用一个辅助函数DFS() 作为算法的关键(标准回溯)。
这两种解决方案的 exist() 函数是:
public boolean exist(char[][] board, String word) {
if (word == null || word.length() == 0) return true;
if (board == null || board.length == 0 || board[0] == null) return false;
if (word.length() > board.length * board[0].length) return false;
char[] w = word.toCharArray();
char a = w[0];
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (board[i][j] == a && DFS(board,w,i,j,0)) return true;
}
}
return false;
}
现在,这是第一种方法的函数DFS()。 (请注意,该程序假定一个单词永远不会包含 @ 字符,但可以通过使用一些随机 Unicode 字符甚至布尔数组来使单词变得更强大,以确保总保证):
解决方案 1
public boolean DFS(char[][] board, char[] word, int i, int j, int idx) {
if (idx == word.length) return true;
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length ||
board[i][j] != word[idx]) return false;
board[i][j] = '@';
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
for (int[] d : dirs) {
if (DFS(board,word,i+d[0],j+d[1],idx+1)) return true;
}
board[i][j] = word[idx];
return false;
}
这是第二个解决方案:
解决方案 2
public boolean DFS(char[][] board, char[] word, int i, int j, int idx) {
if (idx == word.length) return true;
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length ||
board[i][j] != word[idx]) return false;
board[i][j] = '@';
boolean found = false;
found = DFS(board,word,i-1,j,idx+1) || DFS(board,word,i+1,j,idx+1) ||
DFS(board,word,i,j-1,idx+1) || DFS(board,word,i,j+1,idx+1);
board[i][j] = word[idx];
return found;
}
现在,据我了解,由于 Java 短路,如果任何子问题返回 true,DFS() 的两个版本都应该停止探索其他路径。事实上,我可以评估的两个版本之间存在的唯一操作差异是,如果找到解决方案,第一个版本不会打扰重置板(恢复到其原始形式,没有 @ 字符指示访问的位置),而第二个版本可以。
所以据我了解,如果有的话,第二种解决方案应该比第一种解决方案慢。但很明显,我错了:解决方案一在 8 毫秒内运行,但解决方案二在 3 毫秒内运行。
有人知道我在这里可能缺少什么吗?
谢谢
【问题讨论】:
-
1.: 你做过理论上的运行时分析吗? 2.: 为什么在调用
DFS时不使用空格? 3.:查看DFS的来电数量。如果数量相同,则查找是否有不必要的对象创建。 -
我想知道
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};分配是否是问题所在。 Java 应该足够聪明,可以在堆栈上分配它,但谁知道呢。尝试将其移出函数到类范围。让我知道事情的后续。不过,即使在这里,我也不建议过分重视 LC 的运行时间——最好编写自己的基准测试。两个 DFS 例程都应该短路,但解决方案 2 很好地展开,所以没有循环或if。 -
@akuzminykh 1. 你读过这篇文章吗?这些算法实际上是等效的。 2.因为我认为这看起来更好。 3. 同样,算法实际上是等效的,人们不会认为在最多 N^2 个堆栈帧中分配 8 个整数的数组会导致速度降低 2 倍以上。
-
@ggorlen 这是一个很好的可能性,我很高兴你提到了它,因为这个数组确实应该被声明为一个类成员,因为它可以持续重用。有趣的是,进行此更改只会将解决方案 1 的运行时间从 8 毫秒减少到 6 毫秒,因此解决方案 2 的运行速度仍然是原来的两倍
-
如前所述,soln 2 具有展开的循环,不需要索引到数组来获取数据。我将其归类为微优化领域。如果您编写自己的真正推动代码的基准测试,我怀疑您会看到重大差异,如果您这样做,您可以对其进行分析以查看问题所在。运行代码 2-6 毫秒并不能告诉你太多。
标签: java algorithm graph runtime depth-first-search