题目描述(困难难度)

212. Word Search II - 图1

79 题 Word Search 的延续。

意思就是从某个字符出发,然后它可以向左向右向上向下移动,走过的路径构成一个字符串,判断是否能走出给定字符串的 word ,还有一个条件就是走过的字符不能够走第二次。

比如 eat,从第二行最后一列的 e 出发,向左移动,再向左移动,就走出了 eat。

题目中给一个 word 列表,要求找出哪些单词可以由 board 生成。

解法一

直接利用 79 题 的代码 ,79 题是判断某个单词能否在 board 中生成。这里的话,很简单,直接遍历 words 数组,然后利用 79 题的代码去依次判断即可。

79 题 使用的时候采用 dfs ,没做过的话大家可以先过去看一下。

  1. public List<String> findWords(char[][] board, String[] words) {
  2. List<String> res = new ArrayList<>();
  3. //判断每个单词
  4. for (String word : words) {
  5. if (exist(board, word)) {
  6. res.add(word);
  7. }
  8. }
  9. return res;
  10. }
  11. //下边是 79 题的代码
  12. public boolean exist(char[][] board, String word) {
  13. int rows = board.length;
  14. if (rows == 0) {
  15. return false;
  16. }
  17. int cols = board[0].length;
  18. for (int i = 0; i < rows; i++) {
  19. for (int j = 0; j < cols; j++) {
  20. if (existRecursive(board, i, j, word, 0)) {
  21. return true;
  22. }
  23. }
  24. }
  25. return false;
  26. }
  27. private boolean existRecursive(char[][] board, int row, int col, String word, int index) {
  28. if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) {
  29. return false;
  30. }
  31. if (board[row][col] != word.charAt(index)) {
  32. return false;
  33. }
  34. if (index == word.length() - 1) {
  35. return true;
  36. }
  37. char temp = board[row][col];
  38. board[row][col] = '$';
  39. boolean up = existRecursive(board, row - 1, col, word, index + 1);
  40. if (up) {
  41. board[row][col] = temp; //将 board 还原, 79 题中的代码没有还原,这里必须还原
  42. return true;
  43. }
  44. boolean down = existRecursive(board, row + 1, col, word, index + 1);
  45. if (down) {
  46. board[row][col] = temp;//将 board 还原, 79 题中的代码没有还原,这里必须还原
  47. return true;
  48. }
  49. boolean left = existRecursive(board, row, col - 1, word, index + 1);
  50. if (left) {
  51. board[row][col] = temp;//将 board 还原, 79 题中的代码没有还原,这里必须还原
  52. return true;
  53. }
  54. boolean right = existRecursive(board, row, col + 1, word, index + 1);
  55. if (right) {
  56. board[row][col] = temp;//将 board 还原, 79 题中的代码没有还原,这里必须还原
  57. return true;
  58. }
  59. board[row][col] = temp;
  60. return false;
  61. }

然后它竟然过了,竟然过了。。。我还以为这么暴力一定会暗藏玄机。不过为了尊重它是一道 hard 的题目,我就继续思考能不能优化下。

顺着上边的思路想,首先 79 题 中判断单个单词是否能生成肯定不能优化了,不然之前肯定会写优化方法。那么继续优化的话,就只能去寻求不同 word 的之间的联系了。

什么意思呢?

就是如果知道了某个单词能生成,那么对于后边将要判断的单词能不能提供些帮助呢?

或者知道了某个单词不能生成,对于后边将要判断的单词能不能提供些帮助呢?

想了想,只想到了一种情况。比如我们已经知道了 basketboard 能够在二维数组 board 中生成。那么它的所有前缀一定也能生成,比如 basket 一定能够生成。

说到前缀,自然而然的想到了之前的前缀树,这几天出现的频率也比较高,刚做的 211 题 也用到了。我们可以把能生成的 word 加入到前缀树中,然后再判断后边的单词前,先判断它是不是前缀树中某个单词的前缀。

当然如果单词 AB 的前缀,那么 A 的长度肯定短一些,所以我们必须先判断了较长的单词 B,才能产生优化的效果。所以我们首先要把 words 按照单词的长度从大到小排序。

小弟不才,只想到了这一种联系,下边是代码,前缀树直接搬 208 题 的代码即可。

  1. //208 题前缀树代码
  2. class Trie {
  3. class TrieNode {
  4. TrieNode[] children;
  5. boolean flag;
  6. public TrieNode() {
  7. children = new TrieNode[26];
  8. flag = false;
  9. for (int i = 0; i < 26; i++) {
  10. children[i] = null;
  11. }
  12. }
  13. }
  14. TrieNode root;
  15. /** Initialize your data structure here. */
  16. public Trie() {
  17. root = new TrieNode();
  18. }
  19. /** Inserts a word into the trie. */
  20. public void insert(String word) {
  21. char[] array = word.toCharArray();
  22. TrieNode cur = root;
  23. for (int i = 0; i < array.length; i++) {
  24. // 当前孩子是否存在
  25. if (cur.children[array[i] - 'a'] == null) {
  26. cur.children[array[i] - 'a'] = new TrieNode();
  27. }
  28. cur = cur.children[array[i] - 'a'];
  29. }
  30. // 当前节点代表结束
  31. cur.flag = true;
  32. }
  33. /**
  34. * Returns if there is any word in the trie that starts with the given
  35. * prefix.
  36. */
  37. public boolean startsWith(String prefix) {
  38. char[] array = prefix.toCharArray();
  39. TrieNode cur = root;
  40. for (int i = 0; i < array.length; i++) {
  41. if (cur.children[array[i] - 'a'] == null) {
  42. return false;
  43. }
  44. cur = cur.children[array[i] - 'a'];
  45. }
  46. return true;
  47. }
  48. };
  49. //本题代码
  50. public List<String> findWords(char[][] board, String[] words) {
  51. //将单词的长度从大到小排序
  52. Arrays.sort(words, new Comparator<String>() {
  53. @Override
  54. public int compare(String o1, String o2) {
  55. return o2.length() - o1.length();
  56. }
  57. });
  58. Trie trie = new Trie();
  59. List<String> res = new ArrayList<>();
  60. for (String word : words) {
  61. //判断当前单词是否是已经完成的单词的前缀
  62. if (trie.startsWith(word)) {
  63. res.add(word);
  64. continue;
  65. }
  66. if (exist(board, word)) {
  67. res.add(word);
  68. //加入到前缀树中
  69. trie.insert(word);
  70. }
  71. }
  72. return res;
  73. }
  74. //下边是 79 题的代码
  75. public boolean exist(char[][] board, String word) {
  76. int rows = board.length;
  77. if (rows == 0) {
  78. return false;
  79. }
  80. int cols = board[0].length;
  81. for (int i = 0; i < rows; i++) {
  82. for (int j = 0; j < cols; j++) {
  83. if (existRecursive(board, i, j, word, 0)) {
  84. return true;
  85. }
  86. }
  87. }
  88. return false;
  89. }
  90. private boolean existRecursive(char[][] board, int row, int col, String word, int index) {
  91. if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) {
  92. return false;
  93. }
  94. if (board[row][col] != word.charAt(index)) {
  95. return false;
  96. }
  97. if (index == word.length() - 1) {
  98. return true;
  99. }
  100. char temp = board[row][col];
  101. board[row][col] = '$';
  102. boolean up = existRecursive(board, row - 1, col, word, index + 1);
  103. if (up) {
  104. board[row][col] = temp;
  105. return true;
  106. }
  107. boolean down = existRecursive(board, row + 1, col, word, index + 1);
  108. if (down) {
  109. board[row][col] = temp;
  110. return true;
  111. }
  112. boolean left = existRecursive(board, row, col - 1, word, index + 1);
  113. if (left) {
  114. board[row][col] = temp;
  115. return true;
  116. }
  117. boolean right = existRecursive(board, row, col + 1, word, index + 1);
  118. if (right) {
  119. board[row][col] = temp;
  120. return true;
  121. }
  122. board[row][col] = temp;
  123. return false;
  124. }

然而事实是残酷的,对于 leetcodetest cases ,这个想法并没有带来速度上的提升。于是就去逛 discuss 了,也就是下边的解法二。

解法二

参考 这里)。

解法一中的想法是,从 words 中依次选定一个单词 -> 从图中的每个位置出发,看能否找到这个单词

我们其实可以倒过来。从图中的每个位置出发 -> 看遍历过程中是否遇到了 words 中的某个单词

遍历过程中判断是否遇到了某个单词,我们可以事先把所有单词存到前缀树中。这样的话,如果当前走的路径不是前缀树的前缀,我们就可以提前结束了。如果是前缀树的中的单词,我们就将其存到结果中。

至于实现的话,我们可以在遍历过程中,将当前路径的单词传进函数,然后判断当前路径构成的单词是否是在前缀树中出现即可。

这个想法可行,但不够好,因为每次都从前缀树中判断当前路径的单词,会带来重复的判断。比如先判断了 an 存在于前缀树中,接下来假如路径变成 ang ,判断它在不在前缀中,又需要判断一遍 an

因此,我们可以将前缀树融合到我们的算法中,递归中去传递前缀树的节点,判断当前节点的孩子是否为 null,如果是 null 说明当前前缀不存在,可以提前结束。如果不是 null,再判断当前节点是否是单词的结尾,如果是结尾直接将当前单词加入。

由于递归过程中没有加路径,所以我们改造一下前缀树的节点,将单词直接存入节点,这样的话就可以直接取到了。

干巴巴的文字可能不好理解,看一下下边的代码应该就明白了。

  1. //改造前缀树节点
  2. class TrieNode {
  3. public TrieNode[] children;
  4. public String word; //节点直接存当前的单词
  5. public TrieNode() {
  6. children = new TrieNode[26];
  7. word = null;
  8. for (int i = 0; i < 26; i++) {
  9. children[i] = null;
  10. }
  11. }
  12. }
  13. class Trie {
  14. TrieNode root;
  15. /** Initialize your data structure here. */
  16. public Trie() {
  17. root = new TrieNode();
  18. }
  19. /** Inserts a word into the trie. */
  20. public void insert(String word) {
  21. char[] array = word.toCharArray();
  22. TrieNode cur = root;
  23. for (int i = 0; i < array.length; i++) {
  24. // 当前孩子是否存在
  25. if (cur.children[array[i] - 'a'] == null) {
  26. cur.children[array[i] - 'a'] = new TrieNode();
  27. }
  28. cur = cur.children[array[i] - 'a'];
  29. }
  30. // 当前节点结束,存入当前单词
  31. cur.word = word;
  32. }
  33. };
  34. class Solution {
  35. public List<String> findWords(char[][] board, String[] words) {
  36. Trie trie = new Trie();
  37. //将所有单词存入前缀树中
  38. List<String> res = new ArrayList<>();
  39. for (String word : words) {
  40. trie.insert(word);
  41. }
  42. int rows = board.length;
  43. if (rows == 0) {
  44. return res;
  45. }
  46. int cols = board[0].length;
  47. //从每个位置开始遍历
  48. for (int i = 0; i < rows; i++) {
  49. for (int j = 0; j < cols; j++) {
  50. existRecursive(board, i, j, trie.root, res);
  51. }
  52. }
  53. return res;
  54. }
  55. private void existRecursive(char[][] board, int row, int col, TrieNode node, List<String> res) {
  56. if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) {
  57. return;
  58. }
  59. char cur = board[row][col];//将要遍历的字母
  60. //当前节点遍历过或者将要遍历的字母在前缀树中不存在
  61. if (cur == '$' || node.children[cur - 'a'] == null) {
  62. return;
  63. }
  64. node = node.children[cur - 'a'];
  65. //判断当前节点是否是一个单词的结束
  66. if (node.word != null) {
  67. //加入到结果中
  68. res.add(node.word);
  69. //将当前单词置为 null,防止重复加入
  70. node.word = null;
  71. }
  72. char temp = board[row][col];
  73. //上下左右去遍历
  74. board[row][col] = '$';
  75. existRecursive(board, row - 1, col, node, res);
  76. existRecursive(board, row + 1, col, node, res);
  77. existRecursive(board, row, col - 1, node, res);
  78. existRecursive(board, row, col + 1, node, res);
  79. board[row][col] = temp;
  80. }
  81. }

结合代码就很好懂了,就是从每个位置对图做深度优先搜索,然后路径生成的字符串如果没有在前缀树中出现就提前结束,如果到了前缀树中某个单词的结束,就将当前单词加入即可。

受到前边的题目思维的限制,只想到了解法一,优化的话也没有很成功。其实把思路倒过来,解法二也就可以出来了,很有意思。

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情