Skip to content

Commit

Permalink
Improvement of algorithm runtime (#1)
Browse files Browse the repository at this point in the history
Using tree to store word paths
  • Loading branch information
BlasterAlex authored Sep 3, 2024
1 parent 0e71567 commit 5d08df2
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 61 deletions.
130 changes: 77 additions & 53 deletions solution.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import List, Set, NamedTuple
from typing import List, Set, Dict, NamedTuple
import queue


class QueuedWord(NamedTuple):
"""Word in processing queue"""

word: str
"Processed word"

Expand All @@ -13,19 +15,27 @@ class QueuedWord(NamedTuple):
"Level in path relative to beginning/end"


class Solution:
def __init__(self):
self.positionLetters = []
self.wordNeighborsCache = {}
self.wordSet = set()
class WordTreeNode(NamedTuple):
"""Word in hierarchy tree"""

children: Set = set()
"Child words in hierarchy tree"

level: int = 1
"Level in hierarchy tree"

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

class Solution:
wordSet: Set[str]
positionLetters: List[Set[str]]
wordNeighborsCache: Dict[str, Set[str]]
forwardWordTree: Dict[str, WordTreeNode]
backwardWordTree: Dict[str, WordTreeNode]
wordQueue = queue.Queue()

def prepareData(self, beginWord: str, endWord: str, wordList: List[str]):
"""Required data initialization"""

self.positionLetters = []
self.wordNeighborsCache = {}
self.wordSet = set(wordList)
Expand All @@ -37,23 +47,23 @@ def prepareData(self, beginWord: str, endWord: str, wordList: List[str]):
elif c not in self.positionLetters[i]:
self.positionLetters[i].add(c)

self.forwardWordPaths = {}
self.backwardWordPaths = {}
self.forwardVisited = {}
self.backwardVisited = {}
self.forwardWordTree = {}
self.backwardWordTree = {}

if not self.wordQueue.empty():
self.wordQueue = queue.Queue()

for w in self.findWordNeighbors(beginWord):
self.forwardWordPaths[w] = [[beginWord, w]]
self.forwardWordTree[w] = WordTreeNode({beginWord})
self.wordQueue.put(QueuedWord(w, True))

for w in self.findWordNeighbors(endWord):
self.backwardWordPaths[w] = [[w, endWord]]
self.backwardWordTree[w] = WordTreeNode({endWord})
self.wordQueue.put(QueuedWord(w, False))

def findWordNeighbors(self, word: str) -> Set[str]:
"""Getting a list of all adjacent words with result caching"""

if word in self.wordNeighborsCache:
return self.wordNeighborsCache[word]

Expand All @@ -69,48 +79,60 @@ def findWordNeighbors(self, word: str) -> Set[str]:
self.wordNeighborsCache[word] = neighbors
return neighbors

def visitNeighbor(self, qWord: QueuedWord, neighbor: str) -> bool:
"""Checking that this neighbor has not been visited before in this path"""
visited = self.forwardVisited if qWord.forward else self.backwardVisited
word = qWord.word
if word in visited:
if neighbor in visited[word]:
return True
else:
visited[word].add(neighbor)
else:
visited[word] = {neighbor}
return False
def wordPathFound(self, qWord: QueuedWord) -> bool:
"""Checking the condition for finding a new path intersection"""

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

def wordProcessing(self, qWord: QueuedWord):
"""Processing word from word queue"""

word = qWord.word
forward = qWord.forward
level = qWord.level
neighborLevel = qWord.level + 1

for neighbor in self.findWordNeighbors(word):
if self.visitNeighbor(qWord, neighbor):
continue
wordTree = self.forwardWordTree if forward else self.backwardWordTree
if word in wordTree:
if neighbor in wordTree[word].children:
continue

wordPaths = self.forwardWordPaths if forward else self.backwardWordPaths
if forward:
wPaths = [wpath + [neighbor] for wpath in wordPaths[word]]
if neighbor in wordTree:
node = wordTree[neighbor]
if node.level == neighborLevel:
node.children.add(word)
else:
wPaths = [[neighbor] + wpath for wpath in wordPaths[word]]
if len(wPaths) == 0:
continue

self.wordQueue.put(QueuedWord(neighbor, forward, level + 1))
if neighbor in wordPaths:
if len(wPaths[0]) == len(wordPaths[neighbor][0]):
wordPaths[neighbor] += wPaths
wordTree[neighbor] = WordTreeNode({word}, neighborLevel)
self.wordQueue.put(QueuedWord(neighbor, forward, neighborLevel))

def buildCrossPaths(self, point: str) -> List[List[str]]:
"""Getting a list of all word paths that intersect at the given point"""

forwardPaths = self.buildWordPaths(point, True)
backwardPaths = self.buildWordPaths(point, False)
return [forwardPath + backwardPath[1:] for forwardPath in forwardPaths for backwardPath in backwardPaths]

def buildWordPaths(self, word: str, forward: bool) -> List[List[str]]:
"""Getting a list of all word path """

wordTree = self.forwardWordTree if forward else self.backwardWordTree
if word not in wordTree:
return [[word]]

result = []
node = wordTree[word]

for child in node.children:
childPaths = self.buildWordPaths(child, forward)
if forward:
result += [childPath + [word] for childPath in childPaths]
else:
wordPaths[neighbor] = wPaths
result += [[word] + childPath for childPath in childPaths]

return result

def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
if endWord not in wordList:
Expand All @@ -123,22 +145,24 @@ def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List
self.prepareData(beginWord, endWord, wordList)

result = []
foundWords = set()
pathsFound = set()
foundLevel = 0
foundForward = False

# word queue processing
while not self.wordQueue.empty():
qWord = self.wordQueue.get()
if foundLevel > 0:
if qWord in foundWords or qWord.forward != foundForward or qWord.level > foundLevel:
# given path has already been found or all possible points of the same level have been processed
if qWord in pathsFound or qWord.forward != foundForward or qWord.level > foundLevel:
continue
if self.wordFound(qWord):
foundWords.add(qWord)
if self.wordPathFound(qWord):
# word path intersection point is found, save the paths
result += self.buildCrossPaths(qWord.word)
foundLevel, foundForward = qWord.level, qWord.forward
result += [forwardPaths + backwardPaths[1:] for forwardPaths in self.forwardWordPaths[qWord.word] for
backwardPaths in self.backwardWordPaths[qWord.word]]
pathsFound.add(qWord)
else:
# processing word from queue
self.wordProcessing(qWord)

return result
16 changes: 8 additions & 8 deletions test_solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,31 @@

from solution import Solution

LOCAL_TIMEOUT = 5
TEST_TIMEOUT = 1


class TestSolution(unittest.TestCase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.solution = Solution()

@timeout(LOCAL_TIMEOUT)
@timeout(TEST_TIMEOUT)
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"]]
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

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

@timeout(LOCAL_TIMEOUT)
@timeout(TEST_TIMEOUT)
def test_case_19(self):
[beginWord, endWord] = ["qa", "sq"]
wordList = ["si", "go", "se", "cm", "so", "ph", "mt", "db", "mb", "sb", "kr", "ln", "tm", "le", "av", "sm",
Expand Down Expand Up @@ -56,7 +56,7 @@ def test_case_19(self):
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

@timeout(LOCAL_TIMEOUT)
@timeout(TEST_TIMEOUT)
def test_case_21(self):
[beginWord, endWord] = ["cet", "ism"]
wordList = ["kid", "tag", "pup", "ail", "tun", "woo", "erg", "luz", "brr", "gay", "sip", "kay", "per", "val",
Expand Down Expand Up @@ -100,15 +100,15 @@ def test_case_21(self):
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

@timeout(LOCAL_TIMEOUT)
@timeout(TEST_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)

@timeout(LOCAL_TIMEOUT)
@timeout(TEST_TIMEOUT)
def test_case_32(self):
[beginWord, endWord] = ["aaaaa", "ggggg"]
wordList = ["aaaaa", "caaaa", "cbaaa", "daaaa", "dbaaa", "eaaaa", "ebaaa", "faaaa", "fbaaa", "gaaaa", "gbaaa",
Expand Down Expand Up @@ -163,7 +163,7 @@ def test_case_32(self):
actual = self.solution.findLadders(beginWord, endWord, wordList)
self.assertCountEqual(actual, expected)

@timeout(LOCAL_TIMEOUT)
@timeout(TEST_TIMEOUT)
def test_case_34(self):
[beginWord, endWord] = ["cater", "mangy"]
wordList = ["kinds", "taney", "mangy", "pimps", "belly", "liter", "cooks", "finny", "buddy", "hewer", "roves",
Expand Down

0 comments on commit 5d08df2

Please sign in to comment.