Skip to content

Commit

Permalink
Improved solution
Browse files Browse the repository at this point in the history
  • Loading branch information
BlasterAlex committed Sep 1, 2024
1 parent e96e539 commit 4bdf4df
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 50 deletions.
154 changes: 111 additions & 43 deletions solution.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
from typing import List, Set
from typing import List, Set, NamedTuple
import queue


class QueuedWord(NamedTuple):
word: str
forward: bool
level: int = 1


class Solution:
def __init__(self):
self.positionLetters = []
self.wordNeighbors = {}
self.wordNeighborsCache = {}
self.wordSet = set()
self.foundLevel = 0

self.wordQueue = queue.Queue()
self.backwardWordPaths = {}
self.forwardWordPaths = {}
self.backwardVisited = {}
self.forwardVisited = {}

def prepareData(self, endWord: str, wordList: List[str]) -> bool:
def prepareData(self, beginWord: str, endWord: str, wordList: List[str]) -> bool:
if endWord not in wordList:
return False

self.positionLetters = []
self.wordNeighbors = {}
self.wordNeighborsCache = {}
self.wordSet = set(wordList)

for word in wordList:
Expand All @@ -22,11 +35,31 @@ def prepareData(self, endWord: str, wordList: List[str]) -> bool:
self.positionLetters.append({c})
elif c not in self.positionLetters[i]:
self.positionLetters[i].add(c)

if not self.wordQueue.empty():
self.wordQueue = queue.Queue()
self.forwardWordPaths = {}
self.backwardWordPaths = {}
self.forwardVisited = {}
self.backwardVisited = {}
self.foundLevel = 0

for w in self.findWordNeighbors(beginWord):
self.forwardWordPaths[w] = [[beginWord, w]]
self.wordQueue.put(QueuedWord(w, True))
if w == endWord:
self.foundLevel= 1

if self.foundLevel == 0:
for w in self.findWordNeighbors(endWord):
self.backwardWordPaths[w] = [[w, endWord]]
self.wordQueue.put(QueuedWord(w, False))

return True

def findWordNeighbors(self, word: str) -> Set[str]:
if word in self.wordNeighbors:
return self.wordNeighbors[word]
if word in self.wordNeighborsCache:
return self.wordNeighborsCache[word]

neighbors = set()
for i, c in enumerate(word):
Expand All @@ -37,48 +70,83 @@ def findWordNeighbors(self, word: str) -> Set[str]:
if w in self.wordSet:
neighbors.add(w)

self.wordNeighbors[word] = neighbors
self.wordNeighborsCache[word] = neighbors
return neighbors

def visitNeighbor(self, word: str, neighbor: str, forward: bool) -> bool:
visited = self.forwardVisited if forward else self.backwardVisited
if word in visited:
if neighbor in visited[word]:
return True
else:
visited[word].add(neighbor)
else:
visited[word] = {neighbor}
return False

def wordFound(self, qWord: QueuedWord) -> bool:
if qWord.forward:
return qWord.word in self.backwardWordPaths
else:
return qWord.word in self.forwardWordPaths

def wordIteration(self, qWord: QueuedWord):
word = qWord.word
forward = qWord.forward
level = qWord.level

for neighbor in self.findWordNeighbors(word):
if self.visitNeighbor(word, neighbor, forward):
continue

wordPaths = self.forwardWordPaths if forward else self.backwardWordPaths
if forward:
wPaths = [wpath + [neighbor] for wpath in wordPaths[word] if neighbor not in wpath]
else:
wPaths = [[neighbor] + wpath for wpath in wordPaths[word] if neighbor not in wpath]
if len(wPaths) == 0:
continue

self.wordQueue.put(QueuedWord(neighbor, forward, level+1))
if neighbor in wordPaths:
minPathLen = len(wPaths[0])
for p in wordPaths[neighbor]:
if len(p) < minPathLen:
break
else:
wordPaths[neighbor] += wPaths
else:
wordPaths[neighbor] = wPaths

def mergeWordPaths(self, word: str) -> List[List[str]]:
return [f + b[1:] for f in self.forwardWordPaths[word] for b in self.backwardWordPaths[word]]

def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
if not self.prepareData(endWord, wordList):
if not self.prepareData(beginWord, endWord, wordList):
return []

wordQueue = queue.Queue()
wordPaths = dict()
visitedNeighbors = dict()

for w in self.findWordNeighbors(beginWord):
wordPaths[w] = [[beginWord, w]]
wordQueue.put(w)

while not wordQueue.empty():
word = wordQueue.get()
if word == endWord:
return wordPaths[word]

for w in self.findWordNeighbors(word):
if word in visitedNeighbors:
if w in visitedNeighbors[word]:
continue
else:
visitedNeighbors[word].add(w)
else:
visitedNeighbors[word] = {w}
result = []
if self.foundLevel > 0:
while not self.wordQueue.empty():
qWord = self.wordQueue.get()
if qWord.word == endWord:
result += self.forwardWordPaths[qWord.word]
return result

wpaths = [wpath + [w] for wpath in wordPaths[word] if w not in wpath]
if len(wpaths) == 0:
foundWords = set()
foundLevel = 0
foundForward = False

while not self.wordQueue.empty():
qWord = self.wordQueue.get()
if foundLevel > 0:
if qWord in foundWords or qWord.forward != foundForward or qWord.level > foundLevel:
continue
if self.wordFound(qWord):
foundWords.add(qWord)
foundLevel, foundForward = qWord.level, qWord.forward
result += self.mergeWordPaths(qWord.word)
else:
self.wordIteration(qWord)

wordQueue.put(w)
if w in wordPaths:
minPathLen = len(wpaths[0])
for p in wordPaths[w]:
if len(p) < minPathLen:
break
else:
wordPaths[w] += wpaths
else:
wordPaths[w] = wpaths

return []
return result
50 changes: 43 additions & 7 deletions test_solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

from solution import Solution

LOCAL_TIMEOUT = 10
LOCAL_TIMEOUT = 5


class TestSolution(unittest.TestCase):
def __init__(self, *args, **kwargs):
Expand All @@ -15,14 +16,16 @@ def test_case_1(self):
[beginWord, endWord] = ["hit", "cog"]
wordList = ["hot", "dot", "dog", "lot", "log", "cog"]
expected = [["hit", "hot", "dot", "dog", "cog"], ["hit", "hot", "lot", "log", "cog"]]
self.assertCountEqual(self.solution.findLadders(beginWord, endWord, wordList), expected)
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

@timeout(LOCAL_TIMEOUT)
def test_case_2(self):
[beginWord, endWord] = ["hit", "cog"]
wordList = ["hot", "dot", "dog", "lot", "log"]
expected = []
self.assertCountEqual(self.solution.findLadders(beginWord, endWord, wordList), expected)
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

@timeout(LOCAL_TIMEOUT)
def test_case_19(self):
Expand Down Expand Up @@ -50,7 +53,8 @@ def test_case_19(self):
["qa", "pa", "pt", "st", "sq"], ["qa", "ma", "mt", "st", "sq"], ["qa", "la", "lt", "st", "sq"],
["qa", "ma", "mr", "sr", "sq"], ["qa", "ba", "br", "sr", "sq"], ["qa", "ca", "cr", "sr", "sq"],
["qa", "la", "lr", "sr", "sq"], ["qa", "fa", "fr", "sr", "sq"], ["qa", "ta", "tc", "sc", "sq"]]
self.assertCountEqual(self.solution.findLadders(beginWord, endWord, wordList), expected)
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

@timeout(LOCAL_TIMEOUT)
def test_case_21(self):
Expand Down Expand Up @@ -93,9 +97,17 @@ def test_case_21(self):
"err", "him", "all", "pad", "hah", "hie", "aim"]
expected = [['cet', 'cat', 'can', 'ian', 'inn', 'ins', 'its', 'ito', 'ibo', 'ibm', 'ism'],
['cet', 'cot', 'con', 'ion', 'inn', 'ins', 'its', 'ito', 'ibo', 'ibm', 'ism']]
self.assertCountEqual(self.solution.findLadders(beginWord, endWord, wordList), expected)
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

@timeout(LOCAL_TIMEOUT)
def test_case_31(self):
[beginWord, endWord] = ["a", "c"]
wordList = ["a", "b", "c"]
expected = [["a", "c"]]
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

@unittest.skip("current algorithm is not optimal")
@timeout(LOCAL_TIMEOUT)
def test_case_32(self):
[beginWord, endWord] = ["aaaaa", "ggggg"]
Expand Down Expand Up @@ -148,4 +160,28 @@ def test_case_32(self):
['aaaaa', 'aaaaz', 'aaawz', 'aavwz', 'avvwz', 'vvvwz', 'vvvww', 'wvvww', 'wwvww', 'wwwww', 'ywwww', 'yywww',
'yyyww', 'yyyyw', 'yyyyy', 'xyyyy', 'xxyyy', 'xxxyy', 'xxxxy', 'xxxxx', 'gxxxx', 'ggxxx', 'gggxx', 'ggggx',
'ggggg']]
self.assertCountEqual(self.solution.findLadders(beginWord, endWord, wordList), expected)
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

@timeout(LOCAL_TIMEOUT)
def test_case_34(self):
[beginWord, endWord] = ["cater", "mangy"]
wordList = ["kinds", "taney", "mangy", "pimps", "belly", "liter", "cooks", "finny", "buddy", "hewer", "roves",
"lusts", "toots", "fully", "acorn", "junes", "araby", "visas", "pyres", "siren", "limps", "paved",
"marla", "tulsa", "foxes", "purls", "stats", "bidet", "milky", "payee", "horny", "tanks", "mints",
"cindy", "forms", "files", "fucks", "dolts", "welts", "dykes", "riced", "rebel", "gulfs", "bully",
"meets", "tidal", "surer", "gecko", "noyes", "rents", "aaron", "rafts", "roils", "sower", "dicey",
"sties", "jamal", "bases", "locus", "gusts", "briar", "gills", "filly", "mixes", "fjord", "aggie",
"tails", "funks", "freon", "roods", "links", "natal", "melds", "abide", "hardy", "lands", "unpin",
"loges", "weest", "rices", "dicks", "gyros", "hands", "quoit", "hater", "rings", "loxed", "weeds",
"coeds", "handy", "boxer", "jamar", "cokes", "earls", "tings", "haley", "tangy", "hinds", "cater",
"mores", "lloyd", "bayes", "slice", "taker", "piped", "doses", "sides", "gorge", "sorta", "gavel",
"lanes", "wrote", "haney", "monet", "mikes", "bared", "pelts", "fails", "curry", "waken", "jaded",
"halos", "welds", "danes", "assad", "waded", "agree", "bents", "comet", "train", "crags", "fifes",
"rared", "noons", "scums", "steep", "haler", "waxen", "carey", "gamay", "larry", "diver", "honer",
"mandy", "poxed", "coded", "waned", "sades", "clair", "fared", "hangs", "sully", "tiled", "stoic",
"docks", "cloth"]
expected = [["cater", "hater", "haler", "haley", "haney", "taney", "tangy", "mangy"],
["cater", "hater", "haler", "haley", "haney", "handy", "mandy", "mangy"]]
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

0 comments on commit 4bdf4df

Please sign in to comment.