leetcode
leetcode 151 ~ 200
单词搜索 II

单词搜索 II

难度:

标签:

题目描述

给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words, 返回所有二维网格上的单词 。

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

 

示例 1:

输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
输出:["eat","oath"]

示例 2:

输入:board = [["a","b"],["c","d"]], words = ["abcb"]
输出:[]

 

提示:

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 12
  • board[i][j] 是一个小写英文字母
  • 1 <= words.length <= 3 * 104
  • 1 <= words[i].length <= 10
  • words[i] 由小写英文字母组成
  • words 中的所有字符串互不相同

代码结果

运行时间: 468 ms, 内存: 0.0 MB


/*
 * Java Stream API is not suitable for DFS/BFS operations as it's more suitable
 * for functional-style operations on collections. However, we will use streams
 * to collect the results at the end.
 */
import java.util.*;
import java.util.stream.Collectors;
 
public class Solution {
    private static final int[][] DIRECTIONS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
    private int m, n;
    private boolean[][] visited;
 
    public List<String> findWords(char[][] board, String[] words) {
        m = board.length;
        n = board[0].length;
        visited = new boolean[m][n];
        Set<String> result = new HashSet<>();
        TrieNode root = buildTrie(words);
 
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                dfs(board, i, j, root, result);
            }
        }
        // Using Java Stream API to convert Set to List
        return result.stream().collect(Collectors.toList());
    }
 
    private void dfs(char[][] board, int i, int j, TrieNode node, Set<String> result) {
        char c = board[i][j];
        if (c == '#' || node.next[c - 'a'] == null) return;
        node = node.next[c - 'a'];
        if (node.word != null) {   // Found a word
            result.add(node.word);
            node.word = null;   // Avoid duplicate results
        }
        board[i][j] = '#';   // Mark as visited
        for (int[] dir : DIRECTIONS) {
            int x = i + dir[0], y = j + dir[1];
            if (x >= 0 && y >= 0 && x < m && y < n) {
                dfs(board, x, y, node, result);
            }
        }
        board[i][j] = c;   // Restore original character
    }
 
    private TrieNode buildTrie(String[] words) {
        TrieNode root = new TrieNode();
        for (String word : words) {
            TrieNode node = root;
            for (char c : word.toCharArray()) {
                int index = c - 'a';
                if (node.next[index] == null) node.next[index] = new TrieNode();
                node = node.next[index];
            }
            node.word = word;
        }
        return root;
    }
 
    class TrieNode {
        TrieNode[] next = new TrieNode[26];
        String word;
    }
}

解释

方法:

这个题解使用了字典树(Trie)和深度优先搜索(DFS)的方法来解决单词搜索问题。首先,将所有需要搜索的单词构建成一棵字典树。然后,对于网格中的每个字符,从该字符开始进行DFS搜索。在DFS过程中,沿着字典树匹配字符,同时在网格上向四个方向扩展。如果在字典树的某个节点匹配到完整的单词,就将该单词加入到输出结果中。为了避免重复访问网格上的字符,使用一个visited数组来记录已访问过的位置。

时间复杂度:

O(m*n*4^w + k*w)

空间复杂度:

O(k*w)

代码细节讲解

🦆
为什么在字典树(Trie)中每个节点需要有一个`is_word`标志和`word`属性,这两者的作用分别是什么?
`is_word`标志用于指示当前节点是否表示一个完整的单词的结束。这对于区分仅是其他单词前缀的字符序列和实际存储在字典树中作为独立单词的字符序列非常重要。例如,'app'和'apple',在'p'节点上的`is_word`标志表示'app'是一个完整的单词。而`word`属性存储的是从根到当前节点形成的完整单词,这样在找到一个完整单词时可以直接从节点获取,而不需要重新遍历从根到该节点的路径来重建单词。
🦆
在DFS搜索过程中,为什么要使用一个`visited`列表来记录已访问的位置,而不是直接修改`board`上的字符?
使用`visited`列表可以帮助我们记录哪些位置已经被访问过,从而避免在DFS过程中重复访问相同的格子。这样做的好处是避免破坏原始`board`的内容,因为直接修改`board`的字符可能会丢失原始数据,而使用`visited`列表则可以在DFS完成后保留完整的`board`。此外,使用`visited`列表还可以灵活地添加和删除访问记录,便于控制和回溯访问路径。
🦆
DFS函数中,在尝试不同方向进行搜索前,为什么需要检查新的坐标`(new_x, new_y)`是否已经在`visited`列表中?
在DFS中检查新的坐标是否已在`visited`列表中是防止重复访问同一位置的重要步骤。由于每个位置只应在一个搜索路径中被访问一次,这可以确保搜索不会进入循环或重复覆盖已探索的路径。这种检查帮助维护了搜索的正确性和效率,防止无限递归和路径的重复探索。
🦆
在实际操作中,如果`words`列表中的单词非常长,这会如何影响算法的性能?
如果`words`列表中的单词非常长,这将对算法的性能产生几个影响:1. 构建字典树的时间和空间复杂度会增加,因为长单词意味着更多的节点和更深的树结构。2. DFS过程中的搜索深度会增加,这可能导致更高的递归开销和更长的搜索时间。3. 随着单词长度的增加,匹配过程中遍历字典树的路径也变得更长,这可能导致在每一步中检查更多的可能性,从而增加算法的执行时间。因此,单词非常长时,整体的时间和空间需求都会上升,降低算法的效率。

相关问题

单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

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

 

示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

示例 2:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true

示例 3:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false

 

提示:

  • m == board.length
  • n = board[i].length
  • 1 <= m, n <= 6
  • 1 <= word.length <= 15
  • boardword 仅由大小写英文字母组成

 

进阶:你可以使用搜索剪枝的技术来优化解决方案,使其在 board 更大的情况下可以更快解决问题?

不同路径 III

在二维网格 grid 上,有 4 种类型的方格:

  • 1 表示起始方格。且只有一个起始方格。
  • 2 表示结束方格,且只有一个结束方格。
  • 0 表示我们可以走过的空方格。
  • -1 表示我们无法跨越的障碍。

返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目

每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格

 

示例 1:

输入:[[1,0,0,0],[0,0,0,0],[0,0,2,-1]]
输出:2
解释:我们有以下两条路径:
1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2)
2. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2)

示例 2:

输入:[[1,0,0,0],[0,0,0,0],[0,0,0,2]]
输出:4
解释:我们有以下四条路径: 
1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2),(2,3)
2. (0,0),(0,1),(1,1),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,3),(1,3),(2,3)
3. (0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(1,1),(0,1),(0,2),(0,3),(1,3),(2,3)
4. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2),(2,3)

示例 3:

输入:[[0,1],[2,0]]
输出:0
解释:
没有一条路能完全穿过每一个空的方格一次。
请注意,起始和结束方格可以位于网格中的任意位置。

 

提示:

  • 1 <= grid.length * grid[0].length <= 20