Hello, reader ๐๐ฝ ! Welcome to day 35 of the series on Problem Solving. Through this series, I aim to pick up at least one question everyday and share my approach for solving it.
Today, I will be picking up LeetCode's daily challenge problem: 212. Word Search II.
๐ค Problem Statement
- Given an
m x n
board of characters and a list of strings words, return all words on the board. Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.
E.g.:
board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
["eat","oath"]
board = [["a","b"],["c","d"]], words = ["abcb"]
[]
๐ฌ Thought Process - Backtracking (TLE) โ
- This problem is an extension to the 79. Word Search problem. Hence, it's recommended to solve that first.
- If you have solved that, then the first solution you will think of is going through every cell and then starting doing a dfs along with backtracking if the first word being searched matches the board.
- At every dfs call, you can check the cell above, below, left, and right of the current cell.
But in this problem, since we are searching for multiple words, it will not be feasible to perform a bfs + backtracking as it would take
O(m*n*wl*4^mn)
time which is terrible.m * n
=> total number of cells in the matrix- wl => number of words
4^mn
=> for a cell, search is performed on all 4 directions
Hence this is not a feasible solution. Top optimize:
- We will perform search for all the words that have a common prefix together.
- Once we find a word, we immediately stop backtracking.
๐ฌ Thought Process - Backtracking + Tries โ
- We will build a prefix tree (trie) that for the given list of words and mark the end of each word..
- Instead of traversing the board for all the words, we will traverse the board whenever the root of the trie has a matching character on the board until.
- For every matching cell and parent character, we will search across all 4 directions (up, down, left, right) looking for a match between the child character of the parent node and cell.
- This is repeated until we reach the mark of end of word.
For e.g., for the following board =
[["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]]
- The at cell
0,0
in matcheso
with the trie node prefix. - Since this isn't the end of a word, we will traverse this trie starting from node
o
and the board from cell(0,0)
. - From cell
(0,0)
and nodeo
, we will look at cells(0,1)
and (1,0)to find a match for the child of node
owhich is character
a`. - We see another match at
(0,1)
. Hence we continue from this cell to search for child of nodea
which ist
at cells(0,2)
, and(1,1)
as it is not the end of word. - We find a match at cell
(1,1)
. Since this isn't the end of word, we continue our search to cells(1,2)
and(2,1)
for the child of nodet
which ish
. - Another match is found at cell
(2,1)
in the board with letterh
. Since this is an end of the word, we stop here.
- The at cell
This search ensures that instead of searching blindly in every cell, we only continue search from a cell when a matching prefix of a word is found.
๐ฉ๐ฝโ๐ป Solution - Backtracking + Tries
class TrieNode {
Map<Character, TrieNode> children;
boolean isEnd;
String word;
TrieNode() {
children = new HashMap();
}
}
class Solution {
public List<String> findWords(char[][] board, String[] words) {
int m = board.length;
int n = board[0].length;
TrieNode root = addWordsToTrie(words);
List<String> output = new ArrayList();
boolean[][] visited = new boolean[m][n];
for(int i = 0; i<m; i++) {
for(int j = 0; j<n; j++) {
if(!root.children.containsKey(board[i][j])) {
continue;
}
searchBoard(board, i, j, root, visited, output);
}
}
return output;
}
public TrieNode addWordsToTrie(String[] words) {
TrieNode root = new TrieNode();
for(String word: words) {
TrieNode curr = root;
for(int i = 0; i<word.length(); i++) {
char ch = word.charAt(i);
if(!curr.children.containsKey(ch)) {
curr.children.put(ch, new TrieNode());
}
curr = curr.children.get(ch);
}
curr.word = word;
curr.isEnd = true;
}
return root;
}
private void searchBoard(char[][] board, int row, int col, TrieNode root, boolean[][] visited, List<String> output) {
int m = board.length, n = board[0].length;
if(root.isEnd) {
output.add(new String(root.word));
root.isEnd = false;
root.word = null;
}
if(row >= m || row < 0 || col >= n || col < 0 || visited[row][col]) {
return;
}
char currChar = board[row][col];
if(!root.children.containsKey(currChar)) {
return;
}
visited[row][col] = true;
int[][] directions = new int[][] {
{-1, 0}, {1,0}, {0,-1}, {0,1}
};
root = root.children.get(currChar);
for(int[] dir: directions) {
int nRow = dir[0] + row;
int nCol = dir[1] + col;
searchBoard(board, nRow, nCol, root, visited, output);
}
visited[row][col] = false;
}
}
Time Complexity: O(N*M * 4^(max(wordLen))
N = number of rows
M = number of columns
wordLen = length of word in words
- We loop through every cell in the board. And perform dfs call for across 4 cells from a cell.
- The max calls we do is for the longest word in the words array
Space Complexity: O(S + M * N)
N = number of rows
M = number of columns
S = sum of letters of all the words
- Since we are using a prefix tree to store all the letters of the words.
- In the worst case all the words can have different characters and we end up using space for all those letters.
- You can find the link to the GitHub repo for this question here: 212. Word Search II.
Conclusion
That's a wrap for today's problem. If you liked my explanation then please do drop a like/ comment. Also, please correct me if I've made any mistakes or if you want me to improve something!
Thank you for reading!