1+ """
2+ Author : Nelson Mak
3+ Date : April 5, 2026
4+
5+ Task:
6+ Given an m x n grid of characters board and a string word,
7+ return the first valid path of coordinates found that matches
8+ the word in the grid. If the word does not exist, return None.
9+
10+ The word can be constructed from letters of sequentially adjacent cells,
11+ where adjacent cells are horizontally or vertically neighboring.
12+ The same letter cell may not be used more than once.
13+
14+ Example:
15+
16+ Matrix:
17+ ---------
18+ |C|A|T|
19+ |X|Y|S|
20+ |A|B|C|
21+ ---------
22+
23+ Word: "CATS"
24+
25+ Result: [(0, 0), (0, 1), (0, 2), (1, 2)]
26+
27+ Implementation notes:
28+ 1. Use a backtracking (DFS) approach to explore all possible paths.
29+ 2. At each cell, recursively check neighbors, in this question it's (Up, Down, Left, Right).
30+ 3. Maintain a 'visited' set for each coordinate
31+ to ensure cells are not reused within the same search branch.
32+ 4. If a path matches the word, return the list of coordinates.
33+ 5. If a branch fails, 'backtrack' by removing the current cell from
34+ the visited set and the path list to allow for other potential matches.
35+
36+ Similar leetcode question that returns a bool: https://leetcode.com/problems/word-search/
37+ """
38+
39+
40+ def get_point_key (len_board : int , len_board_column : int , row : int , column : int ) -> int :
41+ """
42+ Returns the hash key of matrix indexes.
43+
44+ >>> get_point_key(10, 20, 1, 0)
45+ 200
46+ """
47+
48+ return len_board * len_board_column * row + column
49+
50+
51+ def get_word_path (
52+ board : list [list [str ]],
53+ word : str ,
54+ row : int ,
55+ column : int ,
56+ word_index : int ,
57+ visited_points_set : set [int ],
58+ current_path : list [tuple [int , int ]],
59+ ) -> list [tuple [int , int ]] | None :
60+ """
61+ Return the coordinate path if it's possible to find the word in the grid.
62+ """
63+
64+ if board [row ][column ] != word [word_index ]:
65+ return None
66+
67+ # Track current progress
68+ new_path = current_path + [(row , column )]
69+
70+ # Base case
71+ if word_index == len (word ) - 1 :
72+ return new_path
73+
74+ traverts_directions = [(0 , 1 ), (0 , - 1 ), (- 1 , 0 ), (1 , 0 )]
75+ len_board = len (board )
76+ len_board_column = len (board [0 ])
77+ for direction in traverts_directions :
78+ next_i = row + direction [0 ]
79+ next_j = column + direction [1 ]
80+
81+ if not (0 <= next_i < len_board and 0 <= next_j < len_board_column ):
82+ continue
83+
84+ key = get_point_key (len_board , len_board_column , next_i , next_j )
85+ if key in visited_points_set :
86+ continue
87+
88+ visited_points_set .add (key )
89+ result = get_word_path (
90+ board , word , next_i , next_j , word_index + 1 , visited_points_set , new_path
91+ )
92+
93+ if result is not None :
94+ return result
95+
96+ # Backtrack: remove key to try other paths
97+ visited_points_set .remove (key )
98+
99+ return None
100+
101+
102+ def word_search_path (board : list [list [str ]], word : str ) -> list [tuple [int , int ]] | None :
103+ """
104+ >>> # 1. Word is found case
105+ >>> word_search_path([["C","A","T"],["X","Y","S"],["A","B","C"]], "CATS")
106+ [(0, 0), (0, 1), (0, 2), (1, 2)]
107+
108+ >>> # 2. Word is not found case
109+ >>> word_search_path([["A","B"],["C","D"]], "ABCD") is None
110+ True
111+
112+ >>> # 3. Word is a single char
113+ >>> word_search_path([["A"]], "A")
114+ [(0, 0)]
115+
116+ >>> # 4. Invalid board error (empty list)
117+ >>> word_search_path([], "CAT")
118+ Traceback (most recent call last):
119+ ...
120+ ValueError: The board should be a non empty matrix of single chars strings.
121+
122+ >>> # 5. Invalid word error
123+ >>> word_search_path([["A"]], 123)
124+ Traceback (most recent call last):
125+ ...
126+ ValueError: The word parameter should be a string of length greater than 0.
127+ """
128+ # Validation
129+ board_error_message = (
130+ "The board should be a non empty matrix of single chars strings."
131+ )
132+
133+ # Validate board input
134+ if not isinstance (board , list ) or len (board ) == 0 :
135+ raise ValueError (board_error_message )
136+
137+ for row in board :
138+ if not isinstance (row , list ) or len (row ) == 0 :
139+ raise ValueError (board_error_message )
140+ for item in row :
141+ if not isinstance (item , str ) or len (item ) != 1 :
142+ raise ValueError (board_error_message )
143+
144+ # Validate word input
145+ if not isinstance (word , str ) or len (word ) == 0 :
146+ raise ValueError (
147+ "The word parameter should be a string of length greater than 0."
148+ )
149+
150+ rows = len (board )
151+ cols = len (board [0 ])
152+ # Main entry point
153+ for r in range (rows ):
154+ for c in range (cols ):
155+ # Optimization: only trigger recursion if first char in board matches with first char in word input
156+ if board [r ][c ] == word [0 ]:
157+ key = get_point_key (rows , cols , r , c )
158+ path_result = get_word_path (board , word , r , c , 0 , {key }, [])
159+ if path_result is not None :
160+ return path_result
161+
162+ return None
163+
164+
165+ if __name__ == "__main__" :
166+ import doctest
167+ doctest .testmod ()
0 commit comments