diff --git a/DIRECTORY.md b/DIRECTORY.md index 08e23726..73f289bb 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -38,6 +38,11 @@ * [Test Generate Permutations](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/permutations/generate_permutations/test_generate_permutations.py) * Restore Ip Addresses * [Test Restore Ip Addresses](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/restore_ip_addresses/test_restore_ip_addresses.py) + * Word Search + * [Constants](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/word_search/constants.py) + * [Point](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/word_search/point.py) + * [Test Word Search](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/word_search/test_word_search.py) + * [Test Word Search Two](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/word_search/test_word_search_two.py) * Bfs * Graphs * Dot Dsl @@ -174,6 +179,8 @@ * [Test Three Sum](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/three_sum/test_three_sum.py) * Unique Bsts * [Unique Bsts](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/unique_bsts/unique_bsts.py) + * Word Count + * [Test Word Count](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/word_count/test_word_count.py) ## Bit Manipulation * Counting Bits @@ -882,8 +889,6 @@ * [Test Transpose](https://github.com/BrianLusina/PythonSnips/blob/master/tests/algorithms/test_transpose.py) * [Test Tree Building](https://github.com/BrianLusina/PythonSnips/blob/master/tests/algorithms/test_tree_building.py) * [Test Variable Length Qty](https://github.com/BrianLusina/PythonSnips/blob/master/tests/algorithms/test_variable_length_qty.py) - * [Test Word Count](https://github.com/BrianLusina/PythonSnips/blob/master/tests/algorithms/test_word_count.py) - * [Test Word Search](https://github.com/BrianLusina/PythonSnips/blob/master/tests/algorithms/test_word_search.py) * Cryptography * [Test Atbash Cipher](https://github.com/BrianLusina/PythonSnips/blob/master/tests/cryptography/test_atbash_cipher.py) * [Test Caeser](https://github.com/BrianLusina/PythonSnips/blob/master/tests/cryptography/test_caeser.py) diff --git a/algorithms/backtracking/word_search/README.md b/algorithms/backtracking/word_search/README.md new file mode 100755 index 00000000..9e47eb67 --- /dev/null +++ b/algorithms/backtracking/word_search/README.md @@ -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 + +> 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 + +![Example 1](images/examples/word_search_two_example_1.png) +![Example 2](images/examples/word_search_two_example_2.png) +![Example 3](images/examples/word_search_two_example_3.png) +![Example 4](images/examples/word_search_two_example_4.png) +![Example 5](images/examples/word_search_two_example_5.png) + +## 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. diff --git a/algorithms/backtracking/word_search/__init__.py b/algorithms/backtracking/word_search/__init__.py new file mode 100755 index 00000000..b0ace3e0 --- /dev/null +++ b/algorithms/backtracking/word_search/__init__.py @@ -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 + """ + 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]) + 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 diff --git a/algorithms/backtracking/word_search/constants.py b/algorithms/backtracking/word_search/constants.py new file mode 100644 index 00000000..981ce5ae --- /dev/null +++ b/algorithms/backtracking/word_search/constants.py @@ -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), +) diff --git a/algorithms/backtracking/word_search/images/examples/word_search_two_example_1.png b/algorithms/backtracking/word_search/images/examples/word_search_two_example_1.png new file mode 100644 index 00000000..e52e6cfc Binary files /dev/null and b/algorithms/backtracking/word_search/images/examples/word_search_two_example_1.png differ diff --git a/algorithms/backtracking/word_search/images/examples/word_search_two_example_2.png b/algorithms/backtracking/word_search/images/examples/word_search_two_example_2.png new file mode 100644 index 00000000..e095c4b3 Binary files /dev/null and b/algorithms/backtracking/word_search/images/examples/word_search_two_example_2.png differ diff --git a/algorithms/backtracking/word_search/images/examples/word_search_two_example_3.png b/algorithms/backtracking/word_search/images/examples/word_search_two_example_3.png new file mode 100644 index 00000000..24ede794 Binary files /dev/null and b/algorithms/backtracking/word_search/images/examples/word_search_two_example_3.png differ diff --git a/algorithms/backtracking/word_search/images/examples/word_search_two_example_4.png b/algorithms/backtracking/word_search/images/examples/word_search_two_example_4.png new file mode 100644 index 00000000..00955138 Binary files /dev/null and b/algorithms/backtracking/word_search/images/examples/word_search_two_example_4.png differ diff --git a/algorithms/backtracking/word_search/images/examples/word_search_two_example_5.png b/algorithms/backtracking/word_search/images/examples/word_search_two_example_5.png new file mode 100644 index 00000000..07f5f397 Binary files /dev/null and b/algorithms/backtracking/word_search/images/examples/word_search_two_example_5.png differ diff --git a/algorithms/backtracking/word_search/point.py b/algorithms/backtracking/word_search/point.py new file mode 100644 index 00000000..923d4f97 --- /dev/null +++ b/algorithms/backtracking/word_search/point.py @@ -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) diff --git a/tests/algorithms/test_word_search.py b/algorithms/backtracking/word_search/test_word_search.py similarity index 94% rename from tests/algorithms/test_word_search.py rename to algorithms/backtracking/word_search/test_word_search.py index 99102bca..094f40a7 100755 --- a/tests/algorithms/test_word_search.py +++ b/algorithms/backtracking/word_search/test_word_search.py @@ -1,6 +1,6 @@ import unittest - -from algorithms.word_search import Point, WordSearch +from algorithms.backtracking.word_search.point import Point +from algorithms.backtracking.word_search import WordSearch class WordSearchTests(unittest.TestCase): diff --git a/algorithms/backtracking/word_search/test_word_search_two.py b/algorithms/backtracking/word_search/test_word_search_two.py new file mode 100644 index 00000000..f27be1e4 --- /dev/null +++ b/algorithms/backtracking/word_search/test_word_search_two.py @@ -0,0 +1,74 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.backtracking.word_search import find_strings + +TEST_CASES = [ + ( + [ + ["C", "S", "L", "I", "M"], + ["O", "I", "L", "M", "O"], + ["O", "L", "I", "E", "O"], + ["R", "T", "A", "S", "N"], + ["S", "I", "T", "A", "C"], + ], + ["SLIME", "SAILOR", "MATCH", "COCOON"], + ["SLIME", "SAILOR"], + ), + ( + [ + ["C", "S", "L", "I", "M"], + ["O", "I", "B", "M", "O"], + ["O", "L", "U", "E", "O"], + ["N", "L", "Y", "S", "N"], + ["S", "I", "N", "E", "C"], + ], + ["BUY", "STUFF", "ONLINE", "NOW"], + ["BUY", "ONLINE"], + ), + ( + [ + ["C", "O", "L", "I", "M"], + ["I", "N", "L", "M", "O"], + ["A", "L", "I", "E", "O"], + ["R", "T", "A", "S", "N"], + ["S", "I", "T", "A", "C"], + ], + ["REINDEER", "IN", "RAIN"], + ["IN", "RAIN"], + ), + ( + [ + ["P", "S", "L", "A", "M"], + ["O", "P", "U", "R", "O"], + ["O", "L", "I", "E", "O"], + ["R", "T", "A", "S", "N"], + ["S", "I", "T", "A", "C"], + ], + ["TOURISM", "DESTINED", "POPULAR"], + ["POPULAR"], + ), + ( + [ + ["O", "A", "A", "N"], + ["E", "T", "A", "E"], + ["I", "H", "K", "R"], + ["I", "F", "L", "V"], + ], + ["OATH", "PEA", "EAT", "RAIN"], + ["OATH", "EAT"], + ), +] + + +class WordSearchTwoTestCase(unittest.TestCase): + @parameterized.expand(TEST_CASES) + def test_word_search_two( + self, grid: List[List[str]], words: List[str], expected: List[str] + ): + actual = find_strings(grid, words) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/algorithms/test_word_count.py b/algorithms/word_count/test_word_count.py similarity index 100% rename from tests/algorithms/test_word_count.py rename to algorithms/word_count/test_word_count.py diff --git a/algorithms/word_search/README.md b/algorithms/word_search/README.md deleted file mode 100755 index 2b6d1780..00000000 --- a/algorithms/word_search/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# 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 - -> Note: indexes start counting from 0. diff --git a/algorithms/word_search/__init__.py b/algorithms/word_search/__init__.py deleted file mode 100755 index 2ba70c0e..00000000 --- a/algorithms/word_search/__init__.py +++ /dev/null @@ -1,108 +0,0 @@ -from copy import copy - - -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) - - -# 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), -) - - -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 - """ - 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] diff --git a/datastructures/trees/trie/trie.py b/datastructures/trees/trie/trie.py index 254182ca..b88e5a2d 100644 --- a/datastructures/trees/trie/trie.py +++ b/datastructures/trees/trie/trie.py @@ -85,5 +85,25 @@ def starts_with(self, prefix: str) -> bool: return True + def remove_characters(self, string_to_delete: str): + """ + Removes a string from the trie + """ + node = self.root + child_list = [] + + for c in string_to_delete: + child_list.append([node, c]) + node = node.children[c] + + for pair in reversed(child_list): + parent = pair[0] + child_char = pair[1] + target = parent.children[child_char] + + if target.children: + return + del parent.children[child_char] + def __repr__(self): return f"Trie(root={self.root})"