212. Word Search II
Problem¶
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.
Example 1:
Input: board = [[“o”,”a”,”a”,”n”],[“e”,”t”,”a”,”e”],[“i”,”h”,”k”,”r”],[“i”,”f”,”l”,”v”|”o”,”a”,”a”,”n”],[“e”,”t”,”a”,”e”],[“i”,”h”,”k”,”r”],[“i”,”f”,”l”,”v”]], words = [“oath”,”pea”,”eat”,”rain”]
Output: [“eat”,”oath”]
Example 2:
Input: board = [[“a”,”b”],[“c”,”d”|”a”,”b”],[“c”,”d”]], words = [“abcb”]
Output: []
Constraints:
m == board.length
n == board[i].length
1 <= m, n <= 12
board[i][j]
is a lowercase English letter.1 <= words.length <= 3 * 104
1 <= words[i].length <= 10
words[i]
consists of lowercase English letters.- All the strings of
words
are unique.
Solve¶
DP but fail one¶
python
This just throw TLE a lot on me
Guess just BFS/DFS is good enough.
I finding all possible available appearance of every string, storing they head, and tail, and all the path in between in a cache
dictionary.
To make the storing a bit simpler and lower the memory for storing need, I only store string with length that is a power of 2 (2**n
length), all words is split to the similar length [1, 2, 4, 8, ...]
and check in cache
before being process.
def split(self,w):
splitRes = []
clone = w
p = len(w)
i = 0
while p>0:
q = (p & 1) << i
if q:
splitRes.append(clone[:q])
clone = clone[q:]
i += 1
p >>= 1
return splitRes
def find(self, w, board, cache):
sw = self.split(w)
knowedPath = None
for s in sw:
print(s)
paths = self.findPaths(s, cache)
if len(paths) == 0:
return False
if knowedPath is None:
knowedPath = paths
continue
res = self.combine(knowedPath, paths)
if len(res) == 0:
return False
knowedPath = res
cache[w] = knowedPath
return True
I do it in top-down, so that if it hit cache, then we can return directly, or else we split it again, but in half. This return all available, possible path that string s
appear on the board
.
def findPaths(self, s, cache):
print("+",s)
if s in cache:
return cache[s]
if len(s) == 1:
return []
p1 = self.findPaths(s[:len(s)//2], cache)
p2 = self.findPaths(s[len(s)//2:], cache)
cache[s] = self.combine(p1, p2)
return cache[s]
The combine is where I take the path
check for intersection, and the continuation of the head
, tail
of both path
.
Final implement can be really fast, but going bad when the total possible path goes out of hand.
Time Submitted | Status | Runtime | Memory | Language |
---|---|---|---|---|
08/26/2023 07:29 | Time Limit Exceeded | N/A | N/A | python3 |
class Solution:
def split(self,w):
splitRes = []
clone = w
p = len(w)
i = 0
while p>0:
q = (p & 1) << i
if q:
splitRes.append(clone[:q])
clone = clone[q:]
i += 1
p >>= 1
return splitRes
def isadj(self, tk, hp):
if abs(tk[0] - hp[0]) == 1:
return tk[1] == hp[1]
if abs(tk[1] - hp[1]) == 1:
return tk[0] == hp[0]
return False
def combine(self, ks, ps):
res = []
for k, hk, tk in ks:
for p, hp, tp in ps:
if k.isdisjoint(p) and self.isadj(tk, hp):
res.append((k.union(p), hk, tp))
return res
def findPaths(self, s, cache):
print("+",s)
if s in cache:
return cache[s]
if len(s) == 1:
return []
p1 = self.findPaths(s[:len(s)//2], cache)
p2 = self.findPaths(s[len(s)//2:], cache)
cache[s] = self.combine(p1, p2)
return cache[s]
def find(self, w, board, cache):
sw = self.split(w)
knowedPath = None
for s in sw:
print(s)
paths = self.findPaths(s, cache)
if len(paths) == 0:
return False
if knowedPath is None:
knowedPath = paths
continue
res = self.combine(knowedPath, paths)
if len(res) == 0:
return False
knowedPath = res
cache[w] = knowedPath
return True
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
cache = {}
for r, row in enumerate(board):
for i, c in enumerate(row):
if c not in cache:
cache[c] = []
path = set()
path.add((r,i))
head = (r,i)
tail = (r,i)
cache[c].append((path, head, tail))
res = []
for w in words:
print('==',w)
if self.find(w, board, cache):
res.append(w)
return res
Fallback 1000¶
python
So, in case we have of a lot possible path, this give a fall back needed to process the problem. Just process the first 1000 possible path, if it can’t solve the problem, going with the full join
While this barely can finish the problem, but Leetcode do a add up in TLE calculating (They send all the test as one input, after certain time, you get a TLE), which mean I pass the TLE case, but leaving me no time left to easier case.
class Solution:
def split(self,w):
splitRes = []
clone = w
p = len(w)
i = 0
while p>0:
q = (p & 1) << i
if q:
splitRes.append(clone[:q])
clone = clone[q:]
i += 1
p >>= 1
return splitRes
def isadj(self, tk, hp):
if abs(tk[0] - hp[0]) == 1:
return tk[1] == hp[1]
if abs(tk[1] - hp[1]) == 1:
return tk[0] == hp[0]
return False
def combine(self, ks, ps):
res = []
for k, hk, tk in ks:
for p, hp, tp in ps:
if k.isdisjoint(p) and self.isadj(tk, hp):
res.append((k.union(p), hk, tp))
return res[:1000]
def combineFull(self, ks, ps):
res = []
for k, hk, tk in ks:
for p, hp, tp in ps:
if k.isdisjoint(p) and self.isadj(tk, hp):
res.append((k.union(p), hk, tp))
return res
def findPaths(self, s, cache):
# print("+",s)
if s in cache and (s in self.isFull):
return cache[s][:1000]
if len(s) == 1:
return []
p1 = self.findPaths(s[:len(s)//2], cache)
p2 = self.findPaths(s[len(s)//2:], cache)
cache[s] = self.combine(p1[:1000], p2[:1000])
if len(cache[s]) == 0 and (len(p1) == 1000) or (len(p2) == 1000):
cache[s] = self.findPathsFull(s, cache)
return cache[s][:1000]
def findPathsFull(self, s, cache):
# print("+ full",s)
if (s in cache) and (s in self.isFull):
return cache[s]
if len(s) == 1:
return []
self.isFull.add(s)
p1 = self.findPathsFull(s[:len(s)//2], cache)
p2 = self.findPathsFull(s[len(s)//2:], cache)
cache[s] = self.combineFull(p1, p2)
return cache[s]
def find(self, w, board, cache):
sw = self.split(w)
knowedPath = None
for s in sw:
# print(s)
paths = self.findPaths(s, cache)
if len(paths) == 0:
return False
if knowedPath is None:
knowedPath = paths
continue
res = self.combine(knowedPath, paths)
if len(res) == 0:
return False
knowedPath = res
cache[w] = knowedPath
return True
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
cache = {}
self.isFull = set()
for r, row in enumerate(board):
for i, c in enumerate(row):
if c not in cache:
cache[c] = []
path = set()
path.add((r,i))
head = (r,i)
tail = (r,i)
cache[c].append((path, head, tail))
self.isFull.add(c)
res = []
for w in words:
# print('==',w)
if self.find(w, board, cache):
res.append(w)
return res
What we missing¶
1 <= words.length <= 3 * 10**4
1 <= words[i].length <= 10
This meaning we need to do a lot of small words length. Then the split seem isn’t do that much as we do a rather small string
We will want to reuse path a lot more. with a more precision combine, as we only do combine on self.isadj(tk, hp)
, which it self can be added to cache
instead of checking boardly. So maybe, I need to use some of the head, tail as a cache
input to make this a possible solution.
Yank it, Trie tree solution¶
python
Read and learn¶
There is some Trie Tree that you need to learn and finish this problem? Maybe, but I try and fail on DP, so let have some basic reading this code:
- We create a
TrieNode
helper class/object that havechildren
to cache store all possible path goes through; a end of word flag for what ever? - We then have Trie, the main object, which split word into similar set of character. This mean when we get our self into a spiting node, we can reused the path up to that spiting point as a similarity
- Which also mean, any path in the Trie tree that can’t be found on the
board
we can skip processing them.
While this is a fine use case, it doesn’t help with a repetitive case where there just too many path.
class TrieNode:
def __init__(self):
self.children = {}
self.endOfWord = False
class Trie:
def __init__(self,total_words):
self.root = TrieNode()
self.unfound_words = total_words
def insert(self, word: str) -> None:
cur = self.root
for c in word:
if c not in cur.children:
cur.children[c] = TrieNode()
cur = cur.children[c]
cur.endOfWord = True
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
res = []
M , N = len(board) , len(board[0])
d = Trie(len(words))
for w in words:
d.insert(w)
…Fast for ward to the main solve function
A helper function go, that:
- Take a position
(r,c)
, check for current word stages
, basically like a BFS, and curr_node that keeping up with the Trie tree
def go(r,c,s,cur_node):
if cur_node.endOfWord:
res.append(s)
cur_node.endOfWord = False
d.unfound_words -= 1
if d.unfound_words == 0:
return
if r < 0 or c < 0 or r >= M or c >= N:
return
cur_chr = board[r][c]
if cur_chr not in cur_node.children:
return
cur_root = cur_node
cur_node = cur_node.children[cur_chr]
board[r][c] = '69'
go(r+1,c,s+cur_chr,cur_node)
go(r-1,c,s+cur_chr,cur_node)
go(r,c-1,s+cur_chr,cur_node)
go(r,c+1,s+cur_chr,cur_node)
board[r][c] = cur_chr
if not cur_node.children:
del cur_root.children[cur_chr]
return
- And the main just going into the board one cell by one cell and do BFS on every cell
root = d.root
for i in range(M):
for j in range(N):
if d.unfound_words == 0:
return res
go(i,j,"",root)
return res
So key take away is that we process all the words, at once, on each cell of the board. This make the process easier and stop the process if there isn’t any path can lead to words.
Re-implement¶
The yank implement does seem great, so I keep that in mind and start implement
The first is Trie tree, which I use ["!"]
element to store the word, also keeping track of it parent so that I can cut down some of the TreeNode.
class TrieNode:
def __init__(self, above = None):
self.above = above
self.char = {}
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, w):
p = self.root
for c in w:
if c not in p.char:
p.char[c] = TrieNode(p)
p = p.char[c]
p.char["!"] = w
def remove(self, w):
p = self.root
s = ""
for c in w:
if c in p.char:
p = p.char[c]
s += c
for c in s[::-1]:
if not p.char:
del p.above.char[c]
else:
break
p = p.above
I also do some pretty printing for debug
def __str__(self):
import json
p = self.root
def helper(p):
d = {}
if p is None:
return d
for k in p.char:
if k == "!":
d[k] = p.char["!"]
continue
d[k] = helper(p.char[k])
return d
return json.dumps(helper(p), indent = 4)
In the main function, I use a visited
hash map to keep track of already pass through cell. Get DSP and BFS wrong naming here (This is DFS)
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
tree = Trie()
res = []
visited = set()
for w in words:
tree.insert(w)
m = len(board)
n = len(board[0])
def adj(i,j):
res = []
for x, y in [(i-1,j), (i+1, j), (i, j-1), (i, j+1)]:
if 0 <= x < m and 0 <= y < n and (x,y) not in visited:
res.append((x,y))
return res
def bfs(i,j, node):
visited.add((i,j))
if "!" in node.char:
res.append(node.char["!"])
del node.char["!"]
tree.remove(res[-1])
for x, y in adj(i,j):
c = board[x][y]
if c in node.char:
bfs(x,y, node.char[c])
visited.remove((i,j))
for i, r in enumerate(board):
for j, char in enumerate(r):
if char in tree.root.char:
bfs(i,j, tree.root.char[char])
return res
The final implementation look like this.
Time Submitted | Status | Runtime | Memory | Language |
---|---|---|---|---|
08/26/2023 12:49 | Accepted | 1039 ms | 21.8 MB | python3 |
class TrieNode:
def __init__(self, above = None):
self.above = above
self.char = {}
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, w):
p = self.root
for c in w:
if c not in p.char:
p.char[c] = TrieNode(p)
p = p.char[c]
p.char["!"] = w
def remove(self, w):
p = self.root
s = ""
for c in w:
if c in p.char:
p = p.char[c]
s += c
for c in s[::-1]:
if not p.char:
del p.above.char[c]
else:
break
p = p.above
def __str__(self):
import json
p = self.root
def helper(p):
d = {}
if p is None:
return d
for k in p.char:
if k == "!":
d[k] = p.char["!"]
continue
d[k] = helper(p.char[k])
return d
return json.dumps(helper(p), indent = 4)
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
tree = Trie()
res = []
visited = set()
for w in words:
tree.insert(w)
m = len(board)
n = len(board[0])
def adj(i,j):
res = []
for x, y in [(i-1,j), (i+1, j), (i, j-1), (i, j+1)]:
if 0 <= x < m and 0 <= y < n and (x,y) not in visited:
res.append((x,y))
return res
def bfs(i,j, node):
visited.add((i,j))
if "!" in node.char:
res.append(node.char["!"])
del node.char["!"]
tree.remove(res[-1])
for x, y in adj(i,j):
c = board[x][y]
if c in node.char:
bfs(x,y, node.char[c])
visited.remove((i,j))
for i, r in enumerate(board):
for j, char in enumerate(r):
if char in tree.root.char:
bfs(i,j, tree.root.char[char])
return res
Created : August 26, 2023