Last active
March 7, 2025 15:53
-
-
Save linminhtoo/fa098fce68ae9e7fce71348936d7d396 to your computer and use it in GitHub Desktop.
optimised v2: 99% runtime (3 ms), 65% memory (17.88 mb), optimised v1: 98% runtime (424 ms ), 40% memory (17.98 mb). v1 74% runtime (3537 ms), 87% memory (17.8 mb) https://leetcode.com/problems/word-search/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from collections import Counter | |
DIRS = [ | |
(+1, 0), | |
(0, +1), | |
(-1, 0), | |
(0, -1) | |
] | |
class Solution: | |
''' | |
potential optimisation: | |
if recursion is "bad" and we want to avoid invoking more function calls, | |
we can instead use a stack (list) to do iterative DFS. | |
idea is to append the next set of dfs fn arguments into the stack. | |
then, we pop from the stack, so the most-recently-added arguments. | |
another tricky part is keeping track of visited ij entries in the board. | |
we can use a set of (i,j) tuples to do this, but we need to attach one set per item in the stack. | |
ah, or maybe this is not needed. maybe we can still use the same approach of temporarily overriding ij entries by "#". | |
the memory costs of all this might outweigh the recursive approach. it should be compared empirically. | |
''' | |
def exist(self, board: List[List[str]], word: str) -> bool: | |
len_word = len(word) | |
nr = len(board) | |
nc = len(board[0]) | |
# ok this is subsumed by the counter optimisation below. | |
# take care of single letter word so that we can eliminate base cases | |
# if len_word == 1 and nr == 1 and nc == 1: | |
# return board[0][0] == word[0] | |
# angle of attack 2: | |
# build Counter of character frequencies in board. if any character is short, no pt doing any DFS. | |
# nice, 424 ms, beats 98% now yay!! | |
board_counter = Counter(board[i][j] for i in range(nr) for j in range(nc)) | |
word_counter = Counter(word) | |
for char, counts in word_counter.items(): | |
if board_counter[char] < counts: | |
return False | |
# angle of attack 3: | |
# start from the "rarer" side of word | |
# why? bcos it will have fewer matches in the initial phases, helping to prune DFS searches very early on. | |
# nice!!!! 3 ms, beats 99% hehe | |
if board_counter[word[-1]] < board_counter[word[0]]: | |
word = word[::-1] | |
def dfs(i: int, j: int, char_idx: int): | |
# cannot remove this, will fail test cases where len(word) == 1 | |
# it also fails other test cases | |
# ah, this is needed to check first char of word exists in board! | |
# FIX: check first char matching in double for-loop before starting any dfs | |
# if board[i][j] != word[char_idx]: | |
# return False | |
# a better base case that trims 1 level of recursion | |
# note that this requires checking first char of word matches board[i][j] OUTSIDE of dfs() | |
if char_idx == len_word - 1: | |
return True | |
# mark as used | |
orig, board[i][j] = board[i][j], "#" | |
# check all 4 directions | |
found = False | |
for di, dj in DIRS: | |
new_i, new_j = i + di, j + dj | |
# attack 1: let's try to add more conditions here to limit branching | |
# nice! managed to improve runtime from 3537ms (74%) to 3112ms (87%) | |
# memory use is 17.7mb (96%) | |
if ( | |
(new_i >= 0 and new_i < nr) and \ | |
(new_j >= 0 and new_j < nc) and \ | |
board[new_i][new_j] == word[char_idx + 1] | |
): | |
# we don't need to start a new branch here. we already found the solution | |
if char_idx + 1 == len_word - 1: | |
return True | |
found = dfs(new_i, new_j, char_idx + 1) | |
if found: | |
return True | |
# rmbr to backtrack by restoring original value of ij-th entry in board | |
board[i][j] = orig | |
return found | |
# start a DFS at every position | |
for i in range(nr): | |
for j in range(nc): | |
# only bother with dfs if first char is matching | |
# nice, cut runtime further to 2950ms, 91% | |
if board[i][j] == word[0]: | |
found = dfs(i, j, 0) | |
if found: | |
return True | |
return False |
v2 code with 2 angles of attack
from collections import Counter
DIRS = [
(+1, 0),
(0, +1),
(-1, 0),
(0, -1)
]
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
len_word = len(word)
nr = len(board)
nc = len(board[0])
# take care of single letter word so that we can eliminate base cases
if len_word == 1 and nr == 1 and nc == 1:
return board[0][0] == word[0]
# angle of attack 2:
# build Counter of character frequencies in board. if any character is short, no pt doing any DFS.
# nice, 424 ms, beats 98% now yay!!
board_counter = Counter(board[i][j] for i in range(nr) for j in range(nc))
word_counter = Counter(word)
for char, counts in word_counter.items():
if board_counter[char] < counts:
return False
def dfs(i: int, j: int, char_idx: int):
# cannot remove this, will fail test cases where len(word) == 1
# it also fails other test cases
# ah, this is needed to check first char of word exists in board!
# FIX: check first char matching in double for-loop before starting any dfs
# if board[i][j] != word[char_idx]:
# return False
# a better base case that trims 1 level of recursion
# note that this requires checking first char of word matches board[i][j] OUTSIDE of dfs()
if char_idx == len_word - 1:
return True
# mark as used
orig, board[i][j] = board[i][j], "#"
# check all 4 directions
found = False
for di, dj in DIRS:
new_i, new_j = i + di, j + dj
# attack 1: let's try to add more conditions here to limit branching
# nice! managed to improve runtime from 3537ms (74%) to 3112ms (87%)
# memory use is 17.7mb (96%)
if (
(new_i >= 0 and new_i < nr) and \
(new_j >= 0 and new_j < nc) and \
board[new_i][new_j] == word[char_idx + 1]
):
# we don't need to start a new branch here. we already found the solution
if char_idx + 1 == len_word - 1:
found = True
break
found = dfs(new_i, new_j, char_idx + 1)
if found:
break
# rmbr to backtrack by restoring original value of ij-th entry in board
board[i][j] = orig
return found
# start a DFS at every position
for i in range(nr):
for j in range(nc):
# only bother with dfs if first char is matching
# nice, cut runtime further to 2950ms, 91%
if board[i][j] == word[0]:
found = dfs(i, j, 0)
if found:
return True
return False
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
v1 code with some initial optimizations (trimming recursion depth)