Given a grid of letters and a list of words, find the location of each word as a list of coordinates. Resulting list can be in any order, but coordinates for individual words must be given in order. Letters cannot be reused across words, and letters. Each given word is guaranteed to be in the grid. Consecutive letters of words are either down or to the right (i.e no reversed words or reversed sections of words, only down or to the right).
For example, given the following grid and set of words,
[ ['d', 'r', 'd', 'o', 'r', 's'], ['o', 'b', 'i', 'g', 'n', 'c'], ['g', 'f', 'n', 'm', 't', 'a'], ['x', 's', 'i', 'a', 'n', 't'] ] words1 = [ "dog", "dogma", "cat" ]
output the list of coordinates below:
findWords(grid, words)-> [ [ (1, 5), (2, 5), (3, 5) ], # cat [ (0, 2), (0, 3), (1, 3), (2, 3), (3, 3)], # dogma [ (0, 0), (1, 0), (2, 0) ], # dog ]
In this example, the “dog” in “dogma” cannot be used for the word “dog” since letters cannot be reused.
Advertisement
Answer
Approach
- Find paths that spell words. We only continue a path as long as its a prefix of a word.
- We quickly check if a word is a prefix by using bisect_left to check if it’s found in the list of words (a fast alternative to Trie Tree).
- We gather the list of paths for each word
- We reduce the paths to the non-overlapping ones to satisfy the requirement that no two words share a cell letter.
Code
from bisect import bisect_left def find_words(board, words, x, y, prefix, path): ' Find words that can be generated starting at position x, y ' # Base case # find if current word prefix is in list of words found = bisect_left(words, prefix) # can use binary search since words are sorted if found >= len(words): return if words[found] == prefix: yield prefix, path # Prefix in list of words # Give up on path if what we found is not even a prefix # (there is no point in going further) if len(words[found]) < len(prefix) or words[found][:len(prefix)] != prefix: return # Extend path by one lettter in boarde # Since can only go right and down # No need to worry about same cell occurring multiple times in a given path for adj_x, adj_y in [(0, 1), (1, 0)]: x_new, y_new = x + adj_x, y + adj_y if x_new < len(board) and y_new < len(board[0]): yield from find_words(board, words, x_new, y_new, prefix + board[x_new][y_new], path + [(x_new, y_new)]) def check_all_starts(board, words): ' find all possilble paths through board for generating words ' # check each starting point in board for x in range(len(board)): for y in range(len(board[0])): yield from find_words(board, words, x, y, board[x][y], [(x, y)]) def find_non_overlapping(choices, path): ' Find set of choices with non-overlapping paths ' if not choices: # Base case yield path else: word, options = choices[0] for option in options: set_option = set(option) if any(set_option.intersection(p) for w, p in path): # overlaps with path continue else: yield from find_non_overlapping(choices[1:], path + [(word, option)]) def solve(board, words): ' Solve for path through board to create words ' words.sort() # Get choice of paths for each word choices = {} for word, path in check_all_starts(board, words): choices.setdefault(word, []).append(path) # Find non-intersecting paths (i.e. no two words should have a x, y in common) if len(choices) == len(words): return next(find_non_overlapping(list(choices.items()), []), None)
Tests
Test 1
from pprint import pprint as pp words = [ "dog", "dogma", "cat" ] board = [ ['d', 'r', 'd', 'o', 'r', 's'], ['o', 'b', 'i', 'g', 'n', 'c'], ['g', 'f', 'n', 'm', 't', 'a'], ['x', 's', 'i', 'a', 'n', 't']] pp(solve(board, words))
Output
Test 1 [('dog', [(0, 0), (1, 0), (2, 0)]), ('dogma', [(0, 2), (0, 3), (1, 3), (2, 3), (3, 3)]), ('cat', [(1, 5), (2, 5), (3, 5)])]
Test 2
words = ["by","bat"] board = [ ['b', 'a', 't'], ['y', 'x', 'b'], ['x', 'x', 'y'], ] pp(solve(board, words))
Output
Test 2 [('bat', [(0, 0), (0, 1), (0, 2)]), ('by', [(1, 2), (2, 2)])]