Python: Find a word in a matrix of characters - python

I am trying to create a word game that involves finding words in a matrix of 5x5 characters, like so:
[['a', 'a', 'u', 'r', 'a'],
['m', 'v', 'g', 'n', 'x'],
['a', 'q', 'h', 'y', 'o'],
['p', 'r', 'h', 'l', 'h'],
['v', 'h', 'y', 'o', 'j']]
which I have represented as a list of lists. "xylon" should be found but not "nylon" since this reuses the 'n'. I found a similar problem here but I do not know C.
My current solution involves creating a dictionary for each letter in the word, consisting of a list of tuples of its location on the board like so: {'letter':[(row,column),(row,column)]}. Then, for each letter in the word, I check if each of its locations on the board are compatible with the locations of the previous letter. If it is, I add that location to a new path dictionary.
This fails though for repeating letters and other cases like "nylon" which does return True. It is already rather bulky and confusing and I should probably just start over. Is there a more succinct solution I can employ?
Edits:
To clarify: A word is "in" the grid if there exists a path connecting each letter in the word in the grid. Up, down, left, right, and diagonals are allowed. 'x' is adjacent to 'y', which is adjacent to 'l', and so on. The path needs to have no particular shape as long as each letter is adjacent and no specific letter on the board is used twice. A word can have repeating letters, so "pama" would be allowed because there are multiple 'a's that can be used.
#MSW is correct, the game is boggle, though I am finding this out for the first time!

If you want to check for word membership, your starting point of a dictionary mapping letters to positions is a good one:
letter_positions = {}
for (y, row) in enumerate(board):
for (x, letter) in enumerate(row):
letter_positions.setdefault(letter, []).append((x, y))
From there, your function should keep track of which letters have already been used to make sure it doesn't duplicate them:
def move_valid(position, last_position):
if last_position is None:
return True
return (
abs(position[0] - last_position[0]) <= 1 and
abs(position[1] - last_position[1]) <= 1
)
def find_word(word, used=None):
if word == "":
return []
if used is None:
used = []
letter, rest = word[:1], word[1:]
for position in letter_positions.get(letter) or []:
if position in used:
continue
if not move_valid(position, used and used[-1]):
continue
path = find_word(rest, used + [position])
if path is not None:
return [position] + path
return None
And for example:
>>> find_word("xylon")
[(4, 1), (3, 2), (3, 3), (4, 2), (3, 1)]
>>> find_word("bad")
None
Now, note that the runtime here will be O(not great) because of the position in used (used is a list and will require an O(N) search for each letter position) and the used + [position] and [position] + path (each of which will result in an allocation + copy). In practice this will be ~O(word length ^ 2), but could be improved to ~O(word length) with some more sensible data structures.

Related

Error "index out of range" when working with strings in a for loop in python

I'm very new to python and I'm practicing different exercises.
I need to write a program to decode a string. The original string has been modified by adding, after each vowel (letters ’a’, ’e’, ’i’, ’o’ and ’u’), the letter ’p’ and then that same vowel again.
For example, the word “kemija” becomes “kepemipijapa” and the word “paprika” becomes “papapripikapa”.
vowel = ['a', 'e', 'i', 'o', 'u']
input_word = list(input())
for i in range(len(input_word)):
if input_word[i] in vowel:
input_word.pop(i + 1)
input_word.pop(i + 2)
print(input_word)
The algorithm I had in mind was to detect the index for which the item is a vowel and then remove the following 2 items after this item ,so if input_word[0] == 'e' then the next 2 items (input_word[1], input_word[2]) must be removed from the list. For the sample input zepelepenapa, I get this error message : IndexError: pop index out of range even when I change the for loop to range(len(input_word) - 2) ,again I get this same error.
thanks in advance
The loop will run a number of times equal to the original length of input_word, due to range(len(input_word)). An IndexError will occur if input_word is shortened inside the loop, because the code inside the loop tries to access every element in the original list input_word with the expression input_word[i] (and, for some values of input_word, the if block could even attempt to pop items off the list beyond its original length, due to the (i + 1) and (i + 2)).
Hardcoding the loop definition with a specific number like 2, e.g. with range(len(input_word) - 2), to make it run fewer times to account for removed letters isn't a general solution, because the number of letters to be removed is initially unknown (it could be 0, 2, 4, ...).
Here are a couple of possible solutions:
Instead of removing items from input_word, create a new list output_word and add letters to it if they meet the criteria. Use a helper list skip_these_indices to keep track of indices that should be "removed" from input_word so they can be skipped when building up the new list output_word:
vowel = ['a', 'e', 'i', 'o', 'u']
input_word = list("zepelepenapa")
output_word = []
skip_these_indices = []
for i in range(len(input_word)):
# if letter 'i' shouldn't be skipped, add it to output_word
if i not in skip_these_indices:
output_word.append(input_word[i])
# check whether to skip the next two letters after 'i'
if input_word[i] in vowel:
skip_these_indices.append(i + 1)
skip_these_indices.append(i + 2)
print(skip_these_indices) # [2, 3, 6, 7, 10, 11]
print(output_word) # ['z', 'e', 'l', 'e', 'n', 'a']
print(''.join(output_word)) # zelena
Alternatively, use two loops. The first loop will keep track of which letters should be removed in a list called remove_these_indices. The second loop will remove them from input_word:
vowel = ['a', 'e', 'i', 'o', 'u']
input_word = list("zepelepenapa")
remove_these_indices = []
# loop 1 -- find letters to remove
for i in range(len(input_word)):
# if letter 'i' isn't already marked for removal,
# check whether we should remove the next two letters
if i not in remove_these_indices:
if input_word[i] in vowel:
remove_these_indices.append(i + 1)
remove_these_indices.append(i + 2)
# loop 2 -- remove the letters (pop in reverse to avoid IndexError)
for i in reversed(remove_these_indices):
# if input_word has a vowel in the last two positions,
# without a "p" and the same vowel after it,
# which it shouldn't based on the algorithm you
# described for generating the coded word,
# this 'if' statement will avoid popping
# elements that don't exist
if i < len(input_word):
input_word.pop(i)
print(remove_these_indices) # [2, 3, 6, 7, 10, 11]
print(input_word) # ['z', 'e', 'l', 'e', 'n', 'a']
print(''.join(input_word)) # zelena
pop() removes an item at the given position in the list and returns it. This alters the list in place.
For example if I have:
my_list = [1,2,3,4]
n = my_list.pop()
will return n = 4 in this instance. If I was to print my_list after this operation it would return [1,2,3]. So the length of the list will change every time pop() is used. That is why you are getting IndexError: pop index out of range.
So to solve this we should avoid using pop() since it's really not needed in this situation. The following will work:
word = 'kemija'
vowels = ['a', 'e', 'i', 'o', 'u']
new_word = []
for w in word:
if w in vowels:
new_word.extend([w,'p',w])
# alternatively you could use .append() over .extend() but would need more lines:
# new_word.append(w)
# new_word.append('p')
# new_word.append(w)
else:
new_word.append(w)
decoded_word = ''.join(new_word)
print(decoded_word)

Getting Nth permutation of a sequence and getting back original using its index and modified sequence

I know the most popular permutation algorithms (thanks to wonderful question/answer on SO, and other related sites, such as Wikipedia, etc), but I recently wanted to see if I could get the Nth permutation without exhausting the whole permutation space.
Factorial comes to mind, so I ended up looking at posts such as this one that implements the unrank and rank algorithm, as well as many, many other ones. (Here as mentioned, I take into account other sites as "post")
I stumbled upon this ActiveState recipe which seems like it fit what I wanted to do, but it doesn't support doing the reverse (using the result of the function and reusing the index to get back the original sequence/order).
I also found a similar and related answer on SO: https://stackoverflow.com/a/38166666/12349101
But the same problem as above.
I tried and made different versions of the unrank/rank implementation(s) above, but they require that the sorted sequence be passed as well as the index given by the rank function. If a random (even within the range of the total permutation count) is given, it won't work (most of the time I tried at least).
I don't know how to implement this and I don't think I saw anyone on SO doing this yet. Is there any existing algorithm or way to do this/approach this?
To make things clearer:
Here is the Activestate recipe I posted above (at least the one posted in the comment):
from functools import reduce
def NPerms (seq):
"computes the factorial of the length of "
return reduce(lambda x, y: x * y, range (1, len (seq) + 1), 1)
def PermN (seq, index):
"Returns the th permutation of (in proper order)"
seqc = list (seq [:])
result = []
fact = NPerms (seq)
index %= fact
while seqc:
fact = fact // len (seqc)
choice, index = index // fact, index % fact
result += [seqc.pop (choice)]
return result
As mentioned, this handles doing part of what I mentioned in the title, but I don't know how to get back the original sequence/order using both the result of that function + the same index used.
Say I use the above on a string such as hello world inside a list:
print(PermN(list("hello world"), 20))
This output: ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'd', 'r', 'o', 'l']
Now to see if this can go back to the original using the same index + result of the above:
print(PermN(['h', 'e', 'l', 'l', 'o', ' ', 'w', 'd', 'r', 'o', 'l'], 20))
Output: ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'l', 'r', 'd', 'o']
I think this does what you want, and has the benefit that it doesn't matter what the algorithm behind PermN is:
def NmreP(seq,index):
# Copied from PermN
seqc = list (seq [:])
result = []
fact = NPerms (seq)
index %= fact
seq2 = list(range(len(seq))) # Make a list of seq indices
fwd = PermN(seq2,index) # Arrange them as PermN would
result = [0]*len(seqc) # Make an array to fill with results
for i,j in enumerate(fwd): # For each position, find the element in seqc in the position this started from
result[j] = seqc[i]
return result

How to edit individual character formats in string (beyond just upper())?

I am trying to create a sort of version of Wordle in python (just for practice).
I am having difficulty communicating to the player which letters in their guess match (or closely match) the letters in the target word.
I can highlight matches (i.e. where the letter is in the right place) using uppercase, but I don't know how to differentiate between letters which have a match somewhere in the target word and letters which do not appear at all. The relevant code is below:
def compare_words(word,guess):
W = list(word)# need to turn the strings into list to try to compare each part
G = list(guess)
print(W) # printing just to track the two words
print(G)
result =[ ] # define an empty list for our results
for i in range(len(word)):
if guess[i] == word[i]:
result.append(guess[i].upper())
elif guess[i] in word:
result.append(guess[i])
else:
result.append(" ")
print (result)
return result
# note, previous functions ensure the length of the "word" and "guess" are the same and are single words without digits
x = compare_words("slide","slips")
['s', 'l', 'i', 'd', 'e']
['s', 'l', 'i', 'p', 's']
['S', 'L', 'I', ' ', 's']
As you can see, the direct matches are upper, the other matches are unchanged and the "misses" are left out. This is not what I want, are usually the whole guess is spat back out with font change or colours to indicate the matches.
I have looked into bolding and colours but it all at the point of printing. I need something built into the list itself, but I am unsure if I can do this. Any ideas?
Cheers

Going Back to a Previous Command in Python

I am working on a silly project where I try to print something with the most code possible. What I want it to do is to take an input, split it into a list, and go through every letter on that list. Then it picks a random number from 0 to 26, and sees if the letter it is on matches the letter in a separate list, and if it does, append it to yet another list. It's hard to explain, so here is my current code (not finished):
import random
alphabet = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
inserted_words = []
target_word = input('what word do you want printed? (caps only): ')
target_word.split()
class Transform(object):
def __init__(self, letter, number):
self.letter = letter
self.number = number
def RandomChoose(self, insert):
let1 = random.randint(0, 26)
let2 = alphabet[let1]
if let2 == insert:
inserted_words.append(insert)
else:
RandomChoose()
for x in target_word:
y = alphabet.index(x)
x = Transform(x, y)
x.RandomChoose(x)
print(inserted_words)
But there is one problem. Here it is:
def RandomChoose(self, insert):
let1 = random.randint(0, 26)
let2 = alphabet[let1]
if let2 == insert:
inserted_words.append(insert)
else:
RandomChoose()
I want the code to choose a random letter from my 'alphabet' list, and see if it matches the input. If not, I want it to repeat the code from the beginning. so, if the input is A and the random letter is B, It will repeat, And if the random letter is Q, it repeats again. And so on until it picks A randomly, which will then be appended to 'inserted_words'. I tried recursion but to no avail. If you know what to do, please tell me!
Try this:
def RandomChoose(self, insert):
letter = None
while letter != insert:
letter = random.choice(alphabet)
inserted_words.append(insert)
Also you might as well declare alphabet and and inserted_words as elements of the class and access them from there, because otherwise RandomChoose has no use being a class function.
Furthermore, you're not assigning target_word.split() to anything. Try with:
target_word = target_word.split()
Also not directly relevant to the question, but you can capitalize the input with target_word.upper() - that way you won't have to limit input to already capitalized words.

Recursion, out of memory?

I wrote a function with two parameters. One is an empty string and the other is a string word. My assignment is to use to recursion to reverse the word and place it in the empty string. Just as I think ive got it, i received an "out of memory error". I wrote the code so that so it take the word, turn it into a list, flips it backwards, then places the first letter in the empty string, then deletes the letter out of the list so recursion can happen to each letter. Then it compares the length of the the original word to the length of the empty string (i made a list so they can be compared) so that when their equivalent the recursion will end, but idk
def reverseString(prefix, aStr):
num = 1
if num > 0:
#prefix = ""
aStrlist = list(aStr)
revaStrlist = list(reversed(aStrlist))
revaStrlist2 = list(reversed(aStrlist))
prefixlist = list(prefix)
prefixlist.append(revaStrlist[0])
del revaStrlist[0]
if len(revaStrlist2)!= len(prefixlist):
aStr = str(revaStrlist)
return reverseString(prefix,aStr)
When writing something recursive I try and think about 2 things
The condition to stop the recursion
What I want one iteration to do and how I can pass that progress to the next iteration.
Also I'd recommend getting the one iteration working then worry about calling itself again. Otherwise it can be harder to debug
Anyway so applying this to your logic
When the length of the output string matches the length of the input string
add one letter to the new list in reverse. to maintain progress pass list accumulated so far to itself
I wanted to just modify your code slightly as I thought that would help you learn the most...but was having a hard time with that so I tried to write what i would do with your logic.
Hopefully you can still learn something from this example.
def reverse_string(input_string, output_list=[]):
# condition to keep going, lengths don't match we still have work to do otherwise output result
if len(output_list) < len(list(input_string)):
# lets see how much we have done so far.
# use the length of current new list as a way to get current character we are on
# as we are reversing it we need to take the length of the string minus the current character we are on
# because lists are zero indexed and strings aren't we need to minus 1 from the string length
character_index = len(input_string)-1 - len(output_list)
# then add it to our output list
output_list.append(input_string[character_index])
# output_list is our progress so far pass it to the next iteration
return reverse_string(input_string, output_list)
else:
# combine the output list back into string when we are all done
return ''.join(output_list)
if __name__ == '__main__':
print(reverse_string('hello'))
This is what the recursion will look like for this code
1.
character_index = 5-1 - 0
character_index is set to 4
output_list so far = ['o']
reverse_string('hello', ['o'])
2.
character_index = 5-1 - 1
character_index is set to 3
output_list so far = ['o', 'l']
reverse_string('hello', ['o', 'l'])
3.
character_index = 5-1 - 2
character_index is set to 2
output_list so far = ['o', 'l', 'l']
reverse_string('hello', ['o', 'l', 'l'])
4.
character_index = 5-1 - 3
character_index is set to 1
output_list so far = ['o', 'l', 'l', 'e']
reverse_string('hello', ['o', 'l', 'l', 'e'])
5.
character_index = 5-1 - 4
character_index is set to 0
output_list so far = ['o', 'l', 'l', 'e', 'h']
reverse_string('hello', ['o', 'l', 'l', 'e', 'h'])
6. lengths match just print what we have!
olleh

Categories