-
Notifications
You must be signed in to change notification settings - Fork 2
feat/(lgorithms, backtracking dfs trie): word search #130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
BrianLusina
merged 3 commits into
main
from
feat/algorithms-backtracking-dfs-trie-word-search
Dec 27, 2025
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| # Word Search | ||
|
|
||
| Create a program to solve a word search puzzle. | ||
|
|
||
| In word search puzzles you get a square of letters and have to find specific words in them. | ||
|
|
||
| For example: | ||
|
|
||
| ``` | ||
| jefblpepre | ||
| camdcimgtc | ||
| oivokprjsm | ||
| pbwasqroua | ||
| rixilelhrs | ||
| wolcqlirpc | ||
| screeaumgr | ||
| alxhpburyi | ||
| jalaycalmp | ||
| clojurermt | ||
| ``` | ||
|
|
||
| There are several programming languages hidden in the above square. | ||
|
|
||
| Words can be hidden in all kinds of directions: left-to-right, right-to-left, vertical and diagonal. | ||
|
|
||
| Create a program that given a puzzle and a list of words returns the location of the first and last letter of each word. | ||
|
|
||
| You will be provided with a Point(x, y) class which will be used to display the points of the first and last words of | ||
| the found words. | ||
|
|
||
| You will be required to create a method `search` of class WordSearch that takes in a parameter `word` and searches | ||
| through the provided grid for this word. It must return the Points of thw first and last letter of the word if found | ||
| else return None. | ||
|
|
||
| An e.g. | ||
|
|
||
| ``` python | ||
| puzzle = ('jefblpepre\n' | ||
| 'camdcimgtc\n' | ||
| 'oivokprjsm\n' | ||
| 'pbwasqroua\n' | ||
| 'rixilelhrs\n' | ||
| 'wolcqlirpc\n' | ||
| 'screeaumgr\n' | ||
| 'alxhpburyi\n' | ||
| 'jalaycalmp\n' | ||
| 'clojurermt') | ||
|
|
||
| >>> example = WordSearch(puzzle) | ||
| >>> example.search('clojure') | ||
| (Point(0, 9), Point(6, 9)) | ||
| ``` | ||
|
|
||
| From the above, from the word `clojure`, **c** can be found at point 0,9 and the last letter **e** can be found at poin | ||
| 6, 9 | ||
|
|
||
BrianLusina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| > Note: indexes start counting from 0. | ||
|
|
||
| ---- | ||
|
|
||
| # Word Search 2 | ||
|
|
||
| You are given a list of strings that you need to find in a 2D grid of letters such that the string can be constructed | ||
| from letters in sequentially adjacent cells. The cells are considered sequentially adjacent when they are neighbors to | ||
| each other either horizontally or vertically. The solution should return a list containing the strings from the input | ||
| list that were found in the grid. | ||
|
|
||
| ## Constraints | ||
|
|
||
| - 1 <= rows, columns <= 12 | ||
| - 1 <= words.length <= 3 * 10^3 | ||
| - 1 <= words[i].length <= 10 | ||
| - grid[i][j] is an uppercase English letter | ||
| - words[i] consists of uppercase English letters | ||
| - All the strings are unique | ||
|
|
||
| > Note: The order of the strings in the output does not matter. | ||
|
|
||
| ## Examples | ||
|
|
||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|
|
||
| ## Solution | ||
|
|
||
| By using backtracking, we can explore different paths in the grid to search the string. We can backtrack and explore | ||
| another path if a character is not a part of the search string. However, backtracking alone is an inefficient way to | ||
| solve the problem, since several paths have to be explored to search for the input string. | ||
|
|
||
| By using the trie data structure, we can reduce this exploration or search space in a way that results in a decrease in | ||
| the time complexity: | ||
|
|
||
| - First, we’ll construct the Trie using all the strings in the list. This will be used to match prefixes. | ||
| - Next, we’ll loop over all the cells in the grid and check if any string from the list starts from the letter that | ||
| matches the letter of the cell. | ||
| - Once an letter is matched, we use depth-first-search recursively to explore all four possible neighboring directions. | ||
| - If all the letters of the string are found in the grid. This string is stored in the output result array. | ||
| - We continue the steps of all our input strings. | ||
|
|
||
| ### Time Complexity | ||
|
|
||
| The time complexity will be O(n*3^l), where n is equal to rows * columns, and l is the length of the longest string in | ||
| the list. The factor 3^l means that, in the dfs() function, we have four directions to explore initially, but only three | ||
| choices remain in each cell because one has already been explored. In the worst case, none of the strings will have the | ||
| same prefix, so we cannot skip any string from the list. | ||
|
|
||
| ### Space Complexity | ||
|
|
||
| The space complexity of this solution is O(m) where m is the total count of all the characters in all the strings | ||
| present in the input list. This is actually the size of the trie data structure that is built on the list of words | ||
| provided in the input. | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| from copy import copy | ||
| from typing import List, Set, Tuple | ||
| from algorithms.backtracking.word_search.point import Point | ||
| from algorithms.backtracking.word_search.constants import PLANE_LIMITS | ||
| from datastructures.trees.trie import Trie, TrieNode | ||
|
|
||
|
|
||
| class WordSearch: | ||
| def __init__(self, puzzle): | ||
| """ | ||
| Creates a new word search object | ||
| :ivar self.width will be the length of the width for this word-search object, which is the length of the | ||
| first item in the list. | ||
| It is assumed that all items will have same length | ||
| :ivar self.height will be the height of thw object, in this case, just the length of the list | ||
| :param puzzle: the puzzle which will be a tuple of words separated by newline characters | ||
BrianLusina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| self.rows = puzzle.split() | ||
| self.width = len(self.rows[0]) | ||
| self.height = len(self.rows) | ||
|
|
||
| def search(self, word): | ||
| """ | ||
| Searches for a word in the puzzle | ||
| :param word: word to search for in puzzle | ||
| :return: the points where the word can be found, None if the word does not exist in the puzzle | ||
| :rtype: Point | ||
| """ | ||
| # creates a generator object of points for each letter in the puzzle | ||
| positions = (Point(x, y) for x in range(self.width) for y in range(self.height)) | ||
| for pos in positions: | ||
| for plane_limit in PLANE_LIMITS: | ||
| result = self.find_word( | ||
| word=word, position=pos, plane_limit=plane_limit | ||
| ) | ||
| if result: | ||
| return result | ||
| return None | ||
|
|
||
| def find_word(self, word, position, plane_limit): | ||
| """ | ||
| Finds the word on the puzzle given the word itself, the position (Point(x, y)) and the plane limit | ||
| :param word: the word we are currently searching for, e.g python | ||
| :param position: the current point on cartesian plan for the puzzle e.g Point(0, 0) | ||
| :param plane_limit: the current plan limit, e.g Point(1, 0) | ||
| :return: The Point where the whole word can be found | ||
| :rtype: Point | ||
| """ | ||
| # create a copy of the passed in position | ||
| curr_position = copy(position) | ||
| for let in word: | ||
| if self.find_char(coord_point=curr_position) != let: | ||
| return | ||
| curr_position += plane_limit | ||
| return position, curr_position - plane_limit | ||
|
|
||
| def find_char(self, coord_point): | ||
| """ | ||
| finds a character on the given puzzle | ||
| :param coord_point: The current copy of the current point position being sought through | ||
| :return: | ||
| """ | ||
| if coord_point.x < 0 or coord_point.x >= self.width: | ||
| return | ||
| if coord_point.y < 0 or coord_point.y >= self.height: | ||
| return | ||
| # return the particular letter in the puzzled | ||
| return self.rows[coord_point.y][coord_point.x] | ||
|
|
||
|
|
||
| def find_strings(grid: List[List[str]], words: List[str]) -> List[str]: | ||
| """ | ||
| Finds the strings in the grid | ||
| Args: | ||
| grid (List[List[str]]): The grid to search through | ||
| words (List[str]): The words to search for | ||
| Returns: | ||
| List[str]: The words that were found in the grid | ||
| """ | ||
| trie = Trie() | ||
| for word in words: | ||
| trie.insert(word) | ||
|
|
||
| if not grid or not grid[0]: | ||
| return [] | ||
|
|
||
| rows_count, cols_count = len(grid), len(grid[0]) | ||
BrianLusina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| result = [] | ||
|
|
||
| visited: Set[Tuple[int, int]] = set() | ||
|
|
||
| # directions to move in the grid horizontally and vertically from a given cell | ||
| directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] | ||
|
|
||
| # lambda function to check if the current cell is within the grid | ||
| is_cell_within_grid = lambda r, c: 0 <= r < rows_count and 0 <= c < cols_count | ||
|
|
||
| def dfs(row: int, col: int, node: TrieNode, path: str): | ||
| """ | ||
| Depth-first search to find the words in the grid | ||
| Args: | ||
| row (int): The row of the current cell | ||
| col (int): The column of the current cell | ||
| node (TrieNode): The current node in the trie | ||
| path (str): The current path of the word | ||
| """ | ||
| # check if the current node is a word | ||
| if node.is_end: | ||
| result.append(path) | ||
| # prevent duplicates | ||
| node.is_end = False | ||
| # prune the word from the trie | ||
| trie.remove_characters(path) | ||
|
|
||
| # We don't want to exit early, we want to continue searching for other words this is because from this node | ||
| # other words can potentially be found. | ||
|
|
||
| # mark visited | ||
| visited.add((row, col)) | ||
|
|
||
| # explore neighbors | ||
| for dr, dc in directions: | ||
| new_row, new_col = row + dr, col + dc | ||
| # three specific conditions must be met before calling dfs recursively | ||
| # 1. the new cell must be within the grid | ||
| # 2. the new cell must not be visited | ||
| # 3. the new cell must be a child of the current node | ||
| if ( | ||
| is_cell_within_grid(new_row, new_col) | ||
| and (new_row, new_col) not in visited | ||
| and grid[new_row][new_col] in node.children | ||
| ): | ||
| new_character = grid[new_row][new_col] | ||
| dfs( | ||
| new_row, new_col, node.children[new_character], path + new_character | ||
| ) | ||
|
|
||
| # backtracking, remove the visited cell | ||
| # so that we can explore other paths | ||
| # By removing it, we ensure the cell is available again when the algorithm explores a completely different path | ||
| # from a different starting point | ||
| visited.remove((row, col)) | ||
|
|
||
| for row in range(rows_count): | ||
| for col in range(cols_count): | ||
| char = grid[row][col] | ||
| if char in trie.root.children: | ||
| dfs(row, col, trie.root.children[char], char) | ||
|
|
||
| return result | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| from algorithms.backtracking.word_search.point import Point | ||
|
|
||
| # points on cartesian plan enclosing the word grid | ||
| PLANE_LIMITS = ( | ||
| Point(1, 0), | ||
| Point(1, -1), | ||
| Point(1, 1), | ||
| Point(-1, -1), | ||
| Point(0, -1), | ||
| Point(0, 1), | ||
| Point(-1, 1), | ||
| Point(-1, 0), | ||
| ) |
Binary file added
BIN
+57.4 KB
algorithms/backtracking/word_search/images/examples/word_search_two_example_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+59.5 KB
algorithms/backtracking/word_search/images/examples/word_search_two_example_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+60.6 KB
algorithms/backtracking/word_search/images/examples/word_search_two_example_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+53.4 KB
algorithms/backtracking/word_search/images/examples/word_search_two_example_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+62 KB
algorithms/backtracking/word_search/images/examples/word_search_two_example_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| class Point: | ||
| """ | ||
| Defines the blueprint of a specific point on the word grid. This point will be used to mark the position of | ||
| a word on the cartesian plane. | ||
| """ | ||
|
|
||
| def __init__(self, x, y): | ||
| """ | ||
| Creates a new cartesian point object | ||
| :param x: point on x-axis | ||
| :param y: point on y-axis | ||
| """ | ||
| self.x = x | ||
| self.y = y | ||
|
|
||
| def __repr__(self): | ||
| return "Point({}:{})".format(self.x, self.y) | ||
|
|
||
| def __add__(self, other): | ||
| return Point(self.x + other.x, self.y + other.y) | ||
|
|
||
| def __sub__(self, other): | ||
| return Point(self.x - other.x, self.y - other.y) | ||
|
|
||
| def __eq__(self, other): | ||
| return self.x == other.x and self.y == other.y | ||
|
|
||
| def __ne__(self, other): | ||
| return not (self == other) |
4 changes: 2 additions & 2 deletions
4
tests/algorithms/test_word_search.py → ...ktracking/word_search/test_word_search.py
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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.