LeetCode 79, 212

195 阅读4分钟

LeetCode 79 Word Search

链接:leetcode.com/problems/wo…

方法:DFS

时间复杂度:O(mn4l)

空间复杂度:O(mn + l)

想法:其实这题就是常规的DFS,没法写记忆化。但是这个DFS的递归出口我觉得还是有一定的可学习总结之处的。首先,基本思路就是反正枚举每个格子,然后试一下看从这个地方开始DFS能不能给找出一条word路径来。用一个visited数组来放置同一个地方的重复访问。到这里都还是比较基础的,主要是有两点。

  1. 不能用记忆化。因为当从每个格子为起点开始搜路径的时候,比方说搜到一半,word里面还剩一部分要找,这时候确认可以组成word前半部分的那些格子是不能再重复访问的。那比方说之后你以另一个格子为起点搜,搜到了某个位置,word里面也剩一部分,哪怕位置一样、word剩的长度一样,也不能直接记下来返回,因为这两次搜索当中搜到这个地方的状态是不一样的,第一种情况跟第二种情况是从不同的地方开始搜,到这个点的时候不能够再去访问的位置是不一样的。
  2. 对于DFS的递归出口的考虑。首先,我们用一个变量index记录在word里面我们找到了哪个下标了,那么DFS只会在搜索到的当前位置的格子的字符等于word对应的index这地方的字符的时候,我们才会往下继续搜索,因此当真正在递归里调用自己的时候,他其实已经是在判定完了这俩字符相等不相等之后了。假设说index已经指向了word的最后一个字符,我们知道,如果这个时候,board[x][y]==word.charAt(index),那其实就已经说明了我们搜完了,搜到了一个合法的结果,所以递归可以在index == word.length() - 1的时候结束。如果是按照这样的写法,判定这个位置首先合不合法就应该在index的判断前面,如果不合法直接返回false。这样的写法写出来的递归出口会是:
    if (!isValid(x, y) || visited[x][y] || board[x][y] != word.charAt(index)) { 
        return false; 
    } 
    if (index == word.length() - 1) { 
        return true; 
    }
    
    还有另外一种写法,经常刷LeetCode的老司机应该会觉得按照index去进行DFS的时候,递归出口很多时候是index==word.length(),这个时候返回true,而不是使用index==word.length()-1。这种写法也可以写。相当于说,匹配到word的最后一个字符之后,我们还会向下再递归一层,但我们会保证这一层全部返回true。这种写法写出来会是:
    if (index == word.length()) {
        return true;
    }
    if (!isValid(x, y) || visited[x][y] || board[x][y] != word.charAt(index)) {
        return false;
    }
    
    以上两种都是可以的,但是这道题要避免一边在dfs的开头写递归出口,一边又在往下递归的时候施加限定。比方说这样写:
    if (index == word.length()) {
        return true;
    }
        
    if (board[x][y] != word.charAt(index)) {
        return false;
    }
        
    visited[x][y] = true;
    for (int i = 0; i < 4; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        if (isValid(nx, ny) && !visited[nx][ny]) {
            if (dfs(board, nx, ny, index + 1, word, visited)) {
                return true;
            }
        }
    }
    visited[x][y] = false;
    
    假如这样写的话,比方说样例[["a"]] "a"。先开始index=0,然后这俩字符相等,开始搜,这时候会发现没有下一个位置,因为没有下一个位置,这里就不会在for循环里返回true,会在最后返回false,但其实这个情况应当返回true。

代码:

class Solution {
    private int m, n;
    private int[] dx = new int[] {-1, 0, 1, 0};
    private int[] dy = new int[] {0, -1, 0, 1};
    
    public boolean exist(char[][] board, String word) {
        m = board.length;
        n = board[0].length;
        boolean[][] visited = new boolean[m][n];
        
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (dfs(board, i, j, 0, word, visited)) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    private boolean dfs(char[][] board, int x, int y, int index, String word, boolean[][] visited) {
        if (!isValid(x, y) || visited[x][y] || board[x][y] != word.charAt(index)) {
            return false;
        }
        
        if (index == word.length() - 1) {
            return true;
        }
        
        visited[x][y] = true;
        for (int i = 0; i < 4; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];
            if (dfs(board, nx, ny, index + 1, word, visited)) {
                return true;
            }
        }
        visited[x][y] = false;
        
        return false;
    }
    
    private boolean isValid(int x, int y) {
        return x >= 0 && x < m && y >= 0 && y < n;
    }
}

LeetCode 212 Word Search II

链接:leetcode.com/problems/wo…

方法:Trie+DFS

时间复杂度:O(mn3ll),分析参考www.acwing.com/solution/Le…

空间复杂度:字典树的大小,与字符串集合的前缀情况有关

想法:比较典型的使用Trie进行DFS剪枝的题目。剪枝的思想就是,上去先开一个Trie,把这道题words里面所有的单词全存进去,然后这样的话,比方说我在DFS的时候搜到了一个位置,它这个地方是"a",然后它的四周分别是"b","c""d","e",然后先看一下在Trie这个对应的节点上,到底有没有个child是b或c或d或e,比方说只有ae,那么b、c、d就统统都不要去了,只往e这个地方往下递归。

代码:

class TrieNode {
    public String word;
    public TrieNode[] children;
    
    public TrieNode() {
        this.word = null;
        this.children = new TrieNode[26];
    }
}

class Trie {
    public TrieNode root;
    
    public Trie() {
        this.root = new TrieNode();
    }
    
    public void insert(String s) {
        TrieNode p = root;
        for (int i = 0; i < s.length(); i++) {
            int index = s.charAt(i) - 'a';
            if (p.children[index] == null) {
                p.children[index] = new TrieNode();
            }
            p = p.children[index];
        }
        p.word = s;
    }
}

class Solution {
    private int m, n;
    private int[] dx = {-1, 0, 1, 0};
    private int[] dy = {0, -1, 0, 1};
    
    public List<String> findWords(char[][] board, String[] words) {
        m = board.length;
        n = board[0].length;
        
        Trie trie = new Trie();
        for (String word : words) {
            trie.insert(word);
        }
        
        List<String> res = new ArrayList<>();
        boolean[][] visited = new boolean[m][n];
        
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                dfs(board, i, j, trie.root, visited, res);
            }
        }
        
        return res;
    }
    
    private void dfs(char[][] board, int x, int y, TrieNode node, boolean[][] visited, List<String> res) {
        int index = board[x][y] - 'a';
        if (node.children[index] == null) {
            return;
        }
        
        TrieNode child = node.children[index];
        if (child.word != null) {
            res.add(child.word);
            child.word = null;
        }
        
        visited[x][y] = true;
        
        for (int i = 0; i < 4; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];
            if (isValid(nx, ny) && !visited[nx][ny]) {
                dfs(board, nx, ny, child, visited, res);
            }
        }
        
        visited[x][y] = false;
    }
    
    private boolean isValid(int x, int y) {
        return x >= 0 && x < m && y >= 0 && y < n;
    }
}