🏠

Word Ladder


https://leetcode.com/problems/word-ladder

If the wordlist was a graph including beginWord, finding the shortest transformation sequence would be running Dijkstra!

Creating the adjacency list is just calculating, for all tuples of words, which have 1 different letter. This step is O(n^2)*w, which dominates the complexity. The space will also be quadratic, but only O(n^2), since the adjacency matrix could connect everything.

Algorithm

class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        # Find the index of endWord in wordList
        end_word_idxs = [i for i in range(len(wordList)) if wordList[i] == endWord]
        if not end_word_idxs:
            return 0
        end_i = end_word_idxs[0]

        # This ensured beginWord is in wordList, and gets the index of it (to run Dijkstra)
        begin_word_idxs = [i for i in range(len(wordList)) if wordList[i] == beginWord]
        begin_i = begin_word_idxs[0] if begin_word_idxs else len(wordList)
        if begin_i == len(wordList):
            wordList.append(beginWord)

        # Run Dijkstra
        distance = dijkstra(create_adjacency(wordList), len(wordList), begin_i)[end_i]
        
        # Remember that Dijkstra returns min count of edges, so we must +1.
        return distance + 1 if distance != float('inf') else 0

def are_adjacent(w1: str, w2: str) -> bool:
    differ_count = 0
    for i in range(len(w1)):
        if w1[i] != w2[i]:
            differ_count += 1
        if differ_count > 1:
            return False
    return True

def create_adjacency(words: list[str]) -> dict[int, list[int, int]]:
    adj: dict[int, list[int, int]] = defaultdict(list)
    for i in range(len(words)):
        for j in range(i+1, len(words)):
            if are_adjacent(words[i], words[j]):
                adj[i].append([j, 1])
                adj[j].append([i, 1])
    return adj

# Time: O((V+E)*logV)
# Space: O(V)
def dijkstra(
    adjacency: dict[int, list[list[int]]],
    num_vertices: int, # != len(adjacency) if there are orphan vertices!!
    start_vertex: int,
) -> list[int]:
    # We start with default assumption that distance from start_vertex to every vertex
    # is infinite, except for start_vertex.
    distances = [float('inf')] * (num_vertices+1) # +1 optional; depends on zero-indexed
    distances[start_vertex] = 0
    
    # We only want to visit each vertex once.
    visited: set[int] = set()

    # Use a heap to iteratively pop the shortest distance vertex, 
    # until all vertices are visited
    h = []
    heapq.heappush(h, (0, start_vertex))

    # O(v): While there are still unvisited vertices...
    while len(h) > 0:
        # O(log v): Pop the next shortest distance vertex
        cur_dist, vertex = heapq.heappop(h)
        
        # O(1): Get the vertex's edges and mark the vertex as visited
        edges = adjacency[vertex]
        visited.add(vertex)
        
        # O(e): For every edge...
        for edge in edges:
            [dest_vertex, distance] = edge

            # If the vertex is visited, or we already found a shortest distance, 
            # ignore the edge
            if dest_vertex in visited or distances[dest_vertex] <= cur_dist + distance:
                continue

            # O(log v): Store the shortest distance and add it to the heap, 
            # so that we visit it later
            distances[dest_vertex] = cur_dist + distance
            heapq.heappush(h, (distances[dest_vertex], dest_vertex))

    return distances

Note that BFS is more efficient, because we could find the solution without traversing all levels.

But it’s less intuitive to me, because we must keep a visited set, but isn’t the set different depending on the direction we took?

According to this solution it’s not, but I don’t think I can reliably remember why: https://leetcode.com/problems/word-ladder/solutions/1764371/a-very-highly-detailed-explanation/

 

Issues & PRs welcome ♥️
Powered by Hugo - Theme beautifulhugo