Haijun Si

Leetcode week #9: Graphs

Aug 26, 2024 | monday 7:00 pm

This series will be mainly focused for my own accountability and reference, as well as anyone who wants to follow my journey to becoming a better problem solver. Each week, I will highlight problems that I thought was interesting or difficult. Additionally, other problems that I solved and their solutions are listed below.

  1. πŸ”΄ Word Ladder
  2. 🟑 Number of Islands
  3. 🟑 Max Area of Island
  4. 🟑 Clone Graph
  5. 🟑 Walls and Gates
  6. 🟑 Rotting Oranges
  7. 🟑 Pacific Atlantic Water Flow
  8. 🟑 Surrounded Regions
  9. 🟑 Course Schedule
  10. 🟑 Course Schedule II
  11. 🟑 Graph Valid Tree
  12. 🟑 Number of Connected Components in an Undirected Graph
  13. 🟑 Redundant Connections

Word Ladder

HardGraph

Try it yourself here!

Problem Statement: Given a start word, an end word, and a list of words, determine the number of one-letter transformations needed to transform the start word into the end word.

Edge Cases:

  1. The end word may not be in the word list.

Example:

Input: beginWord = β€œhit”, endWord = β€œcog”, wordList = [β€œhot”,β€œdot”,β€œdog”,β€œlot”,β€œlog”,β€œcog”]

Output: 5,

Explanation: hit -> hot -> dot -> dog -> log -> cog, it takes 5 transformations from hot to cog

Solution:

Key Steps:

  1. Check if the end word is in the word list. If not, return 0.
  2. Build a mapping from possible single-letter transformation patterns to lists of words.
  1. Use BFS to explore each pattern the start word can transform into, adding the corresponding words from the pattern mapping to the queue.
  2. Return the number of transformations when the end word is reached. If the end word is not found, continue until all possibilities are exhausted.
  3. Return 0 if the transformation sequence is not possible.

BFS Algorithm Details:

Complexity:

def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
    if endWord not in wordList:
        return 0
    
    graph = defaultdict(list)
    wordList.append(beginWord)
    for word in wordList:
        for j in range(len(word)):
            pattern = word[:j] + "*" + word[j + 1:]
            graph[pattern].append(word)
    seen = set([beginWord])
    q = deque([beginWord])
    res = 1
    while q:
        for i in range(len(q)):
            word = q.popleft()
            if word == endWord:
                return res
            for j in range(len(word)):
                pattern = word[:j] + "*" + word[j + 1:]
                for n in graph[pattern]:
                    if n not in seen:
                        seen.add(n)
                        q.append(n)
        res += 1
    return 0    

Number of Islands

MediumGraphs

Try it yourself here!

Solution:

def numIslands(self, grid: List[List[str]]) -> int:
    res = 0
    def bfs(i, j):
        directions = [(0, 1), (1, 0), (-1, 0), (0, -1)]
        q = deque([(i, j)])
        M, N = len(grid), len(grid[0])
        while q:
            x, y = q.popleft()
            grid[x][y] = "0"
            for dx, dy in directions:
                if 0 <= x + dx < M and 0 <= y + dy < N and grid[x + dx][y + dy] == "1":
                    q.append((x + dx, y + dy))


    for i in range(len(grid)):
        for j in range(len(grid[0])):
            if grid[i][j] == "1":
                res += 1
                bfs(i, j)
    
    return res

Complexity:

Max Area of Island

MediumGraphs

Try it yourself here!

Solution:

def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
    directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
    def dfs(i, j):
        if (i < 0 or i >= len(grid) 
            or j < 0 or j >= len(grid[0]) or grid[i][j] == 0):
            return 0
        
        grid[i][j] = 0
        return 1 + dfs(i + 1, j) + dfs(i, j + 1) \
                    + dfs(i - 1, j) + dfs(i, j - 1)
    maxArea = 0
    for i in range(len(grid)):
        for j in range(len(grid[0])):
            if grid[i][j] == 1:
                area = dfs(i, j)
                maxArea = max(maxArea, area)
    return maxArea

Complexity:

Clone Graph

MediumGraph

Try it yourself here!

Solution:

def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
    newNodes = {}
    def cloneNode(node):
        if not node:
            return
        if node in newNodes:
            return newNodes[node]
        newNode = Node(node.val)
        newNodes[node] = newNode
        for n in node.neighbors:
            neighborNode = cloneNode(n)
            newNode.neighbors.append(neighborNode)
        return newNode
    return cloneNode(node)

Complexity:

Walls and Gates

MediumGraphs

Try it yourself here!

Solution:

def wallsAndGates(self, rooms: List[List[int]]) -> None:
        """
        Do not return anything, modify rooms in-place instead.
        """
        dir = [[0, 1], [1, 0], [0, -1], [-1, 0]]
        queue = deque()
        seen = set()

        for m in range(len(rooms)):
            for n in range(len(rooms[0])):
                if rooms[m][n] == 0:
                    queue.append([m, n])
                    seen.add((m, n))
        while queue:
            x, y = queue.popleft()
            for dx, dy in dir:
                new_x, new_y = x + dx, y + dy
                if 0 <= new_x < len(rooms) and 0 <= new_y < len(rooms[0]) and rooms[new_x][new_y] != -1:
                    rooms[new_x][new_y] = min(rooms[new_x][new_y], rooms[x][y] + 1)
                    if (new_x, new_y) not in seen:
                        queue.append([new_x, new_y])
                        seen.add((new_x, new_y))

Complexity:

Rotting Oranges

MediumGraphs

Try it yourself here!

Solution:

def orangesRotting(self, grid: List[List[int]]) -> int:
    num_oranges = 0
    time = 0
    q = deque()
    rows, cols = len(grid), len(grid[0])
    directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
    for i in range(rows):
        for j in range(cols):
            if grid[i][j] == 1:
                num_oranges += 1
            elif grid[i][j] == 2:
                q.append([i, j])
    
    while q:
        length = len(q)
        for i in range(length):
            [i, j] = q.popleft()
            for dx, dy in directions:
                next_i, next_j = i + dx, j + dy
                if 0 <= next_i < rows and 0 <= next_j < cols and grid[next_i][next_j] == 1:
                    grid[next_i][next_j] = 2
                    num_oranges -= 1
                    q.append([next_i, next_j])
        if not q:
            break
        time += 1
    return -1 if num_oranges else time

Complexity:

Pacific Atlantic Water Flow

MediumGraph

Try it yourself here!

Solution:

def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:   
    atlanticQueue = deque()
    pacificQueue = deque()
  
    rows = len(heights)
    cols = len(heights[0])
    for i in range(rows):
        atlanticQueue.append((i, 0))
        pacificQueue.append((i, cols - 1))
    
    for j in range(cols):
        atlanticQueue.append((0, j))
        pacificQueue.append((rows - 1, j))
    
    def bfs(queue):
        nonlocal rows, cols
        reachable = set()
        while queue:
            i, j = queue.popleft()
            reachable.add((i, j))
            for x, y in [[0, 1], [1, 0], [0, -1], [-1, 0]]:
                dx, dy = i + x, j + y
                if 0 <= dx < rows and 0 <= dy < cols and (dx, dy) not in reachable and heights[dx][dy] >= heights[i][j]:
                    queue.append((dx, dy))
        return reachable
  
    atlantic = bfs(atlanticQueue)
    pacific = bfs(pacificQueue)
  
    return list(atlantic.intersection(pacific))

Complexity:

Surrounded Regions

Medium

Try it yourself here!

Solution:

def solve(self, board: List[List[str]]) -> None:
    """
    Do not return anything, modify board in-place instead.
    """
    def dfs(i, j):
        if board[i][j] != "O":
            return
        board[i][j] = "E"
        if i > 0:
            dfs(i - 1, j)
        if i < len(board) - 1:
            dfs(i + 1, j)
        if j > 0:
            dfs(i, j - 1)
        if j < len(board[0]) - 1:
            dfs(i, j + 1)
    borders = []
    seen = set()
    rows = len(board)
    cols = len(board[0])
    for i in range(rows):
        borders.append((i, cols - 1))
        borders.append((i, 0))


    for j in range(cols):
        borders.append((rows - 1, j))
        borders.append((0, j))
    
    for i, j in borders:
        dfs(i, j)
    
    for i in range(rows):
        for j in range(cols):
            if board[i][j] == "O":
                board[i][j] = "X"
            if board[i][j] == "E":
                board[i][j] = "O"

Complexity:

Course Schedule

MediumGraph

Try it yourself here!

Solution:

def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
    graph = defaultdict(list)
    numPrerequisites = [0] * numCourses

    for a, b in prerequisites:
        graph[b].append(a)
        numPrerequisites[a] += 1
    
    q = deque()
    for i in range(numCourses):
        if numPrerequisites[i] == 0:
            q.append(i)

    nodesVisited = 0
    while q:
        course = q.popleft()
        nodesVisited += 1
        for n in graph[course]:
            numPrerequisites[n] -= 1
            if numPrerequisites[n] == 0:
                q.append(n)
    return nodesVisited == numCourses

Complexity:

Course Schedule 2

MediumGraphs

Try it yourself here!

Solution:

def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
    graph = defaultdict(list)
    degrees = [0] * numCourses
    res = []

    for pair in prerequisites:
        graph[pair[1]].append(pair[0])
        degrees[pair[0]] += 1
    
    q = deque()
    for i in range(numCourses):
        if degrees[i] == 0:
            q.append(i)
            res.append(i)
    nodesVisited = 0
    while q:
        node = q.popleft()
        nodesVisited += 1
        for neighbor in graph[node]:
            degrees[neighbor] -= 1
            if degrees[neighbor] == 0:
                q.append(neighbor)
                res.append(neighbor)



    return res if nodesVisited == numCourses else []

Complexity:

Graph Valid Tree

MediumGraphs

Try it yourself here!

Solution:

def validTree(self, n: int, edges: List[List[int]]) -> bool:
    graph = defaultdict(list)
    for a, b in edges:
        graph[a].append(b)
        graph[b].append(a)

    seen = set()
    def dfs(curr, prev):
        if curr in seen:
            return False
        seen.add(curr)
        for n in graph[curr]:
            if n == prev:
                continue
            if not dfs(n, curr):
                return False
        return True
    return dfs(0, -1) and len(seen) == n

Complexity:

Number of Connected Components in an undirected graph

MediumGraphs

Try it yourself here!

Solution:

def countComponents(self, n: int, edges: List[List[int]]) -> int:
      counter = 0
      graph = defaultdict(list)
      for a, b in edges:
          graph[a].append(b)
          graph[b].append(a)

      seen = set()
      def dfs(node):
          if node in seen:
              return
          seen.add(node)
          for v in graph[node]:
              dfs(v)
      for i in range(n):
          if i not in seen:
              counter += 1
              dfs(i)
      return counter

Complexity:

Redudant Connections

MediumGraphs

Try it yourself here!

Solution:

def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
    graph = defaultdict(set)
    def dfs(source, target):
        if source not in seen:
            seen.add(source)
            if source == target: return True
            for n in graph[source]:
                if dfs(n, target):
                    return True
        return False
    for a, b in edges:
        seen = set()
        if a in graph and b in graph and dfs(a, b):
            return a, b
        graph[a].add(b)
        graph[b].add(a)

Complexity: