Jc-alt logo
jc
data structures and algorithms

LeetCode: Graphs II Hierholzers Eulers

LeetCode: Graphs II Hierholzers Eulers
10 min read
#data structures and algorithms

Hierholzers Algorithm Intro

Intro

Hierholzers Algorithm is used to find a Eulerian path or Eulerian circuit in a graph.

Eulerian Path:

  • Visits every edge exactly once
  • May start and end at different vertices

Eulerian Circuit:

  • Eulerian Path that starts and ends at the same vertex

The algorithm efficiently constructs that path by traversing edges while removing them, ensuring every edge is visited exactly once.

Graph Requirements

  1. Directed or Undirected

  2. Must satisfy Eulerian conditions:

    • Eulerian Circuit: Directed: Every vertex has equal in and out degrees Undirected: Every vertex has even degrees

    • Eulerian Path: Exactly 0 or 2 vertices have unequal degree

  3. Represented Using:

    • Adjacency List (preferred for edge removal)
    • Edge list also works

Output

Eulerian Path or Eulerian Circuit:

  • list of vertices in the order of traversal
  • visited every edge exactly once

Video Animation

Hierholzers: https://www.youtube.com/watch?v=qZrfK2iE4UA

Pseudo Code

    def hierholzer(graph, start):

        path = []        # final Eulerian path/circuit
        stack = [start]  # stack to track current path

        while stack:
            u = stack[-1]
            if graph[u]:
                v = graph[u].pop()   # remove edge from u to v
                stack.append(v)
            else:
                path.append(stack.pop())

        return path[::-1]  # reverse to get correct order

Time Complexity

Each edge is visited and removed exactly once: O(E)

Space Complexity

Stack: Up to O(V) in worst case Path Storage: O(E + 1) vertices

O(V + E)

IRL Use Case

  • Route Planning Postman problems (traverse each street exactly once)

  • Puzzle Solving Draw shapes without lifting a pen

332. Reconstruct Itinerary ::1:: - Hard

Topics: Depth First Search, Graph, Eulerian Circuit

Intro

You are given a list of airline tickets where tickets[i] = [fromi, toi] represent the departure and the arrival airports of one flight. Reconstruct the itinerary in order and return it. All of the tickets belong to a man who departs from "JFK", thus, the itinerary must begin with "JFK". If there are multiple valid itineraries, you should return the itinerary that has the smallest lexical order when read as a single string. For example, the itinerary ["JFK", "LGA"] has a smaller lexical order than ["JFK", "LGB"]. You may assume all tickets form at least one valid itinerary. You must use all the tickets once and only once.

Example InputOutput
tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]["JFK","MUC","LHR","SFO","SJC"]
tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]["JFK","ATL","JFK","SFO","ATL","SFO"]

Constraints:

1 ≤ tickets.length ≤ 300

tickets[i].length == 2

fromi.length == 2

toi.length == 3

fromi and toi consist of uppercase English letters.

fromi != toi

Abstraction

Given a graph, determine the flight path.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Hierholzer's] [Eulerian Path] Hierholzer's Algorithm Eulerian Path via DFS + MinHeap for Lexical Order - Advanced Graphs/Advanced Graphs

    def findItinerary(self, tickets: List[List[str]]) -> List[str]:
        
        # Hierholzer's Algorithm / DFS on Graphs:
        # Hierholzer is similar to DFS on trees, but adapted for graphs with cycles.
        # Key differences:
        #   1. DFS on Trees:
        #        - Visits each node exactly once
        #        - Appends nodes in post-order (after visiting children)
        #        - No cycles, so edges are implicitly used exactly once
        #   2. Hierholzer DFS on Graphs:
        #        - Visits each edge exactly once (nodes can be visited multiple times)
        #        - Removes edges as they are used to prevent revisiting
        #        - Appends nodes in post-order after all outgoing edges are traversed
        #        - Handles cycles and constructs Eulerian paths or circuits
        #   3. Analogy:
        #        - Tree DFS explores nodes, Hierholzer DFS explores edges
        #        - Post-order ensures correct construction of paths in both
        # Result:
        #   - DFS-like traversal that builds a valid Eulerian path using every edge exactly once

        # Hierholzer’s Algorithm:
        # Designed to construct an Eulerian path efficiently.
        # Core Idea: 
        #   - Recursively follow edges, removing them as you go, using each edge exactly once.
        #   - PostOrder Construction: append airports after visiting all outbound edges,
        #                             so the path is built in reverse.
        #   - Lexical Order Preserved: if multiple edges exist, minHeap allows to choose the smallest airport
        
        # Eulerian Path:
        # A trail in a graph that visits every edge exactly once, allowing for revisiting vertices.
        # - In the context of flight tickets:
        #     - Each ticket represents a directed edge from one airport to another.
        #     - We must use all tickets exactly once, starting from "JFK".
        #     - We are allow to revisit airports

        # Graph Existence rules:
        #   1. At most: one node has (out-degree - in-degree) = 1 (starting node)
        #   2. At most: one node has (in-degree - out-degree) = 1 (ending node)
        #   3. All other nodes have equal degrees (in-degree == out-degree).

        # Directed Graph (Adjacency List):
        #   - Vertices: airports
        #   - Edges: flight tickets (from src -> dest)
        #   - Neighbors: destinations reachable from a given airport
        #   - MinHeap ensures lexical order when multiple choices exist

        # Note:
        # 1. Build graph: graph[src] = MinHeap of destinations
        #    (MinHeap ensures lexical order when multiple choices exist)
        # 2. Use DFS to traverse graph:
        #      Always choose the smallest lexical destination first
        #      Remove tickets (edges) as we use them
        # 3. Append airports to itinerary in post-order (after visiting neighbors/edges)
        # 4. Reverse itinerary at the end to get correct order starting at JFK
        # Result -> shortest path through all airports starting at jfk


        # Build adjacency list with MinHeaps
        # sc: O(V), number of vertices
        graph = defaultdict(list)
        
        # Build MinHeap for each source airport
        # sc: O(E), one entry per edge used
        for src, dest in tickets:
            heapq.heappush(graph[src], dest)
        
        # itinerary build in reverse (post order)
        # sc: O(E), one entry per edge used
        itinerary = []

        def dfs(airport):

            # Explore all neighbors in lexical order
            # tc: each edge processed once -> O(E)
            while graph[airport]:
                # tc: O(log E) per pop
                next_dest = heapq.heappop(graph[airport])
                dfs(next_dest)
            
            # PostOrder append to itinerary
            # sc: O(1) per append, overall O(E)
            itinerary.append(airport)

        # Start Eulerian Path via DFS + MinHeap from "JFK"
        # tc: O(E log E) for all recursive DFS calls with heap pops
        dfs("JFK")
        
        # Reverse to get correct order
        # tc: O(E), sc: O(E) for reversed list
        res = itinerary[::-1]

        # overall: tc O(e log e)
        # overall: sc O(e + v)
        return res

2097. Valid Arrangement of Pairs ::1:: - Hard

Topics: Array, Depth First Search, Graph Theory, Eulerian Circuit

Intro

You are given a 0-indexed 2D integer array pairs where pairs[i] = [starti, endi]. An arrangement of pairs is valid if for every index i where 1 lte i lt pairs.length, we have endi-1 == starti. Return any valid arrangement of pairs. Note: The inputs will be generated such that there exists a valid arrangement of pairs.

Example InputOutput
pairs = [[5,1],[4,5],[11,9],[9,4]][[11,9],[9,4],[4,5],[5,1]]
pairs = [[1,3],[3,2],[2,1]][[1,3],[3,2],[2,1]]
pairs = [[1,2],[1,3],[2,1]][[1,2],[2,1],[1,3]]

Constraints:

1 ≤ pairs.length ≤ 10^5

pairs[i].length == 2

0 ≤ starti, endi ≤ 10^9

starti != endi

No two pairs are exactly the same.

There exists a valid arrangement of pairs

Abstraction

I have no idea!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Hierholzer's] [Eulerian Path] Hierholzer's Algorithm Eulerian Path via DFS + MinHeap for Lexical Order - Advanced Graphs/Advanced Graphs

    def validArrangement(self, pairs: List[List[int]]) -> List[List[int]]:
        graph = defaultdict(list)
        indeg = defaultdict(int)
        outdeg = defaultdict(int)

        # Build graph + degrees
        for u, v in pairs:
            graph[u].append(v)
            outdeg[u] += 1
            indeg[v] += 1

        # Find starting node
        start = pairs[0][0]
        for node in graph:
            if outdeg[node] - indeg[node] == 1:
                start = node
                break

        path = []

        # Hierholzer DFS
        def dfs(node):
            while graph[node]:
                nxt = graph[node].pop()
                dfs(nxt)
            path.append(node)

        dfs(start)

        path.reverse()

        # Convert nodes -> edge pairs
        res = []
        for i in range(len(path) - 1):
            res.append([path[i], path[i + 1]])

        return res

753. Cracking the Safe ::1:: - Hard

Topics: Array, Depth First Search, Graph Theory, Eulerian Circuit

Intro

There is a safe protected by a password. The password is a sequence of n digits where each digit can be in the range [0, k - 1]. The safe has a peculiar way of checking the password. When you enter in a sequence, it checks the most recent n digits that were entered each time you type a digit. For example, the correct password is "345" and you enter in "012345": After typing 0, the most recent 3 digits is "0", which is incorrect. After typing 1, the most recent 3 digits is "01", which is incorrect. After typing 2, the most recent 3 digits is "012", which is incorrect. After typing 3, the most recent 3 digits is "123", which is incorrect. After typing 4, the most recent 3 digits is "234", which is incorrect. After typing 5, the most recent 3 digits is "345", which is correct and the safe unlocks. Return any string of minimum length that will unlock the safe at some point of entering it.

Example InputOutput
n = 1, k = 2"10"
n = 2, k = 2"01100"

Constraints:

1 ≤ n ≤ 4

1 ≤ k ≤ 10

1 ≤ k^n ≤ 4096

Abstraction

I have no idea. wow!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Hierholzer's] [Eulerian Path] Hierholzer's Algorithm Eulerian Path via DFS + MinHeap for Lexical Order - Advanced Graphs/Advanced Graphs

    def crackSafe(self, n: int, k: int) -> str:
        start = "0" * (n - 1)
        visited = set()
        res = []

        def dfs(node):
            for x in map(str, range(k)):
                edge = node + x
                if edge not in visited:
                    visited.add(edge)
                    dfs(edge[1:])
                    res.append(x)

        dfs(start)

        # Add starting node to complete sequence
        return start + "".join(reversed(res))