On This Page

What Are These?

This page covers a handful of graph algorithms that go beyond basic BFS/DFS traversals and shortest-path algorithms. Each solves a specific structural question about a graph. They come up in interviews at top companies (especially Google and Meta) and are essential in competitive programming.

Graph Coloring

Graph coloring assigns a label (called a "color") to each vertex (a node in the graph) such that no two adjacent vertices (nodes connected by an edge) share the same color. The minimum number of colors needed for a valid coloring is the chromatic number, usually written χ(G).

This problem has a surprisingly rich history. In 1852, Francis Guthrie noticed that the counties of England could be colored with just four colors such that no neighboring counties shared a color. He conjectured this was true for any map. The four-color theorem — that any planar graph (a graph that can be drawn on a plane without edge crossings) can be colored with at most 4 colors — wasn't proved until 1976, and the proof required a computer to check 1,936 configurations. It remains one of the most famous results in mathematics.

Practical uses of graph coloring:

Eulerian Paths & Circuits

An Eulerian path visits every edge exactly once. An Eulerian circuit (or Eulerian cycle) does the same but starts and ends at the same vertex. Named after Leonhard Euler, who in 1736 proved that the famous Königsberg bridge problem had no solution — making it arguably the first theorem in graph theory and the birth of the entire field.

The Königsberg problem: the city had four land masses connected by seven bridges. Could you walk a route crossing each bridge exactly once? Euler showed this was impossible because more than two land masses had an odd number of bridges (odd degree — the number of edges connected to a vertex).

The conditions are clean and easy to check:

Hamiltonian Paths & Circuits

A Hamiltonian path visits every vertex exactly once. A Hamiltonian circuit visits every vertex exactly once and returns to the start. Despite the superficial similarity to Eulerian paths (edges vs vertices), the computational difference is enormous.

Determining whether a Hamiltonian path exists is NP-complete — no known polynomial-time algorithm exists, and most computer scientists believe none ever will. The Traveling Salesman Problem (TSP), one of the most famous problems in all of computer science, is essentially "find the shortest Hamiltonian circuit in a weighted graph."

For small graphs (n ≤ 20), you can solve Hamiltonian path with bitmask DP in O(n² · 2n). For larger graphs, you need heuristics or approximation algorithms. The bitmask tracks which vertices have been visited: dp[mask][v] = "can we visit exactly the vertices in mask, ending at v?"

Strongly Connected Components (SCC)

In a directed graph, a strongly connected component is a maximal group of vertices where every vertex can reach every other vertex through directed paths. "Maximal" means you can't add any more vertices and maintain the property.

Think of a website link graph. If page A links to page B, and page B (perhaps through a chain of other pages) links back to A, they're in the same SCC. Within an SCC, information can flow freely in any direction. Between SCCs, flow is one-way.

Once you find all SCCs, you can condense the graph: replace each SCC with a single super-node. The resulting graph is always a DAG (directed acyclic graph) — no cycles can exist between different SCCs, by definition. This condensation simplifies many problems because DAGs support topological sorting and straightforward DP.

Applications of SCCs:

Articulation Points & Bridges

An articulation point (or cut vertex) is a vertex whose removal disconnects the graph. A bridge (or cut edge) is an edge whose removal disconnects the graph. Both are found using a modified DFS in O(V + E) — essentially the same algorithm as Tarjan's SCC, but applied to undirected graphs.

These matter for network reliability. An articulation point is a single point of failure. A bridge is a single link whose failure splits the network. Identifying them is critical in network design, circuit analysis, and infrastructure planning.

How They Work

Greedy Graph Coloring

The simplest coloring algorithm: process vertices one at a time. For each vertex, look at the colors assigned to its already-processed neighbors. Assign the smallest color (starting from 0) that none of them are using.

This greedy approach does not guarantee the minimum number of colors — that problem (finding the chromatic number) is NP-hard for general graphs. But it does guarantee at most Δ+1 colors, where Δ (Delta) is the maximum degree in the graph. Brooks' theorem tightens this: every connected graph can be colored with at most Δ colors, except for complete graphs and odd cycles.

The vertex processing order affects the result dramatically. The Welsh-Powell algorithm sorts vertices by degree in descending order before greedy coloring, which tends to produce better results. For bipartite graphs, you always need exactly 2 colors — and detecting bipartiteness is just BFS/DFS 2-coloring.

Tarjan's SCC Algorithm — Step by Step

Tarjan's algorithm finds all SCCs in a single DFS traversal — O(V + E) time. It's elegant but tricky to understand, so let's go piece by piece.

Every node gets two numbers:

The algorithm maintains a stack of vertices in the current DFS path. When we finish processing a vertex v and find that low[v] == ids[v], that means v is the root of its SCC — no vertex in v's subtree can reach anything discovered before v. We pop everything off the stack down to and including v; that's one complete SCC.

Trace through a 5-node example: A→B, B→C, C→A, B→D, D→E:

  1. Visit A (id=0, low=0). Push A. Recurse to B.
  2. Visit B (id=1, low=1). Push B. Recurse to C.
  3. Visit C (id=2, low=2). Push C. Edge C→A: A is on stack, so low[C] = min(2, ids[A]) = 0.
  4. Back to B: low[B] = min(1, low[C]) = 0. Recurse to D.
  5. Visit D (id=3, low=3). Push D. Recurse to E.
  6. Visit E (id=4, low=4). Push E. No outgoing edges. low[E]==ids[E], so E is an SCC root. Pop E. SCC: {E}.
  7. Back to D: low[D]==ids[D], so D is an SCC root. Pop D. SCC: {D}.
  8. Back to B, then A: low[A]==ids[A], so A is an SCC root. Pop C, B, A. SCC: {A, B, C}.

Final SCCs: {A, B, C}, {D}, {E}. Correct — A, B, C form a cycle, while D and E are not part of any cycle.

Kosaraju's Algorithm — The Two-Pass Alternative

Kosaraju's algorithm is conceptually simpler than Tarjan's:

  1. Do a DFS on the original graph. Record vertices in finish order (post-order — the order you finish processing each vertex, not discover it).
  2. Build the reverse graph (flip all edge directions).
  3. Process vertices from step 1 in reverse finish order. Do DFS on the reverse graph. Each DFS tree is one SCC.

Why does this work? In the original graph, vertices with later finish times can reach vertices with earlier finish times. In the reverse graph, this relationship flips. Processing in reverse finish order ensures that each DFS in the reverse graph captures exactly the vertices that can reach each other in both directions — which is the definition of an SCC.

Hierholzer's Algorithm (Eulerian Circuits)

Given a graph with an Eulerian circuit (all degrees even, connected), Hierholzer's algorithm constructs the circuit in O(E) time:

  1. Start at any vertex. Follow edges, deleting them as you go, until you return to the start. This gives you a circuit, but it might not use all edges.
  2. Walk along the circuit you found. Whenever you hit a vertex that still has unused edges, start a new sub-circuit from there (same process: follow and delete until you return).
  3. Splice the sub-circuit into the main circuit at that vertex.
  4. Repeat until all edges are used.

In practice, this is implemented with a stack and edge deletion. The "splicing" happens naturally when you use a stack-based DFS approach.

Bridges & Articulation Points

Found using Tarjan's bridge-finding algorithm (not the SCC one — similar idea, different context). During DFS on an undirected graph:

Common Mistakes

⚠️ Tarjan's: Confusing ids and low for Back Edges When you find a back edge to vertex w (already on the stack), update low[v] = min(low[v], ids[w]), NOT low[w]. Using low[w] is a common variation that happens to work for SCC detection, but it gives wrong results for bridge-finding. Stick with ids[w] for the on-stack back edge update to be safe across both applications.
⚠️ Forgetting the "On Stack" Check in Tarjan's SCC In Tarjan's SCC algorithm, you must only update low[v] from a neighbor w if w is currently on the stack. If w was visited and already assigned to a completed SCC (popped from stack), it's in a different component — don't let it pull your low-link value down. Missing this check produces incorrect SCC decompositions.
⚠️ Euler Path: Forgetting the Connectivity Check Having the right degree conditions (all even, or exactly 2 odd) is necessary but not sufficient for an Eulerian path/circuit. The graph also needs to be connected (ignoring isolated vertices). A graph with two disconnected cycles has all even degrees but no Eulerian circuit. Always check connectivity first.
⚠️ Graph Coloring: Assuming Greedy is Optimal Greedy coloring with vertex ordering can use far more colors than necessary. On a crown graph (Kn,n minus a perfect matching), greedy can use n colors when the chromatic number is just 2 (it's bipartite). If you need the chromatic number, greedy alone won't cut it. For interview purposes, know that optimal coloring is NP-hard and greedy is an approximation.
⚠️ Hamiltonian Path: Trying Polynomial Solutions If you catch yourself writing an O(n²) or O(n³) algorithm for Hamiltonian path, stop. The problem is NP-complete. For n ≤ 20, use bitmask DP (O(n² · 2ⁿ)). For larger n, use backtracking with pruning or approximation. There is no polynomial-time exact algorithm (unless P = NP, in which case you've earned a million dollars).

Interactive Visualization

Graph Coloring & SCC Detection

Coloring mode: click nodes to cycle colors. SCC mode: runs Tarjan's and highlights components.

Operations & Complexity

AlgorithmTimeSpaceNotes
Greedy ColoringO(V + E)O(V)Not optimal, uses ≤ Δ+1 colors
Optimal ColoringNP-hardExact solution is exponential
Bipartite Check (2-color)O(V + E)O(V)BFS or DFS
Tarjan's SCCO(V + E)O(V)Single DFS pass
Kosaraju's SCCO(V + E)O(V + E)Two DFS passes + reverse graph
Bridges (Tarjan's)O(V + E)O(V)Modified DFS
Articulation PointsO(V + E)O(V)Modified DFS
Eulerian Circuit CheckO(V + E)O(1)Check degree parity + connectivity
Hierholzer's (Euler)O(E)O(E)Constructs the circuit/path
Hamiltonian (bitmask DP)O(n² · 2ⁿ)O(n · 2ⁿ)Practical for n ≤ 20

Implementation

Tarjan's SCC Algorithm

Python

def tarjan_scc(graph):
    """Find all Strongly Connected Components using Tarjan's algorithm.
    graph: dict of node -> [neighbors] (adjacency list for directed graph).
    Returns list of SCCs, each SCC is a list of nodes.
    Time: O(V + E), Space: O(V)."""
    idx = [0]           # mutable counter for discovery time
    stack = []           # DFS stack of active nodes
    on_stack = set()     # O(1) lookup for "is node on stack?"
    ids = {}             # discovery time per node
    low = {}             # lowest reachable discovery time
    sccs = []            # result: list of components

    def dfs(v):
        ids[v] = low[v] = idx[0]
        idx[0] += 1
        stack.append(v)
        on_stack.add(v)

        for w in graph.get(v, []):
            if w not in ids:
                # Tree edge: w hasn't been visited yet
                dfs(w)
                low[v] = min(low[v], low[w])
            elif w in on_stack:
                # Back edge: w is an ancestor in the current DFS path.
                # It's in the same SCC candidate as v.
                low[v] = min(low[v], ids[w])
            # If w was visited but NOT on stack, it's in a
            # completed SCC — don't update low[v].

        # Root of SCC: no vertex in v's subtree can reach
        # anything discovered before v.
        if low[v] == ids[v]:
            component = []
            while True:
                w = stack.pop()
                on_stack.remove(w)
                component.append(w)
                if w == v:
                    break
            sccs.append(component)

    for v in graph:
        if v not in ids:
            dfs(v)
    return sccs

# Example:
# graph = {'A':['B'], 'B':['C','D'], 'C':['A'], 'D':['E'], 'E':[]}
# tarjan_scc(graph) → [['E'], ['D'], ['C','B','A']]
      

Kosaraju's SCC Algorithm

Python

def kosaraju_scc(graph):
    """Two-pass SCC algorithm. Conceptually simpler than Tarjan's.
    1. DFS on original graph, record finish order.
    2. DFS on reversed graph in reverse finish order.
    Time: O(V + E), Space: O(V + E) for the reversed graph."""
    # Pass 1: DFS on original graph, record finish order
    visited = set()
    finish_order = []

    def dfs1(v):
        visited.add(v)
        for w in graph.get(v, []):
            if w not in visited:
                dfs1(w)
        finish_order.append(v)  # post-order

    for v in graph:
        if v not in visited:
            dfs1(v)

    # Build reverse graph
    rev = {}
    for v in graph:
        rev.setdefault(v, [])
        for w in graph[v]:
            rev.setdefault(w, []).append(v)

    # Pass 2: DFS on reverse graph in reverse finish order
    visited.clear()
    sccs = []

    def dfs2(v, component):
        visited.add(v)
        component.append(v)
        for w in rev.get(v, []):
            if w not in visited:
                dfs2(w, component)

    for v in reversed(finish_order):
        if v not in visited:
            comp = []
            dfs2(v, comp)
            sccs.append(comp)

    return sccs
      
JavaScript

function tarjanSCC(graph) {
  let idx = 0;
  const stack = [], onStack = new Set();
  const ids = {}, low = {};
  const sccs = [];

  function dfs(v) {
    ids[v] = low[v] = idx++;
    stack.push(v);
    onStack.add(v);

    for (const w of (graph[v] || [])) {
      if (ids[w] === undefined) {
        dfs(w);
        low[v] = Math.min(low[v], low[w]);
      } else if (onStack.has(w)) {
        low[v] = Math.min(low[v], ids[w]);
      }
    }

    if (low[v] === ids[v]) {
      const comp = [];
      let w;
      do {
        w = stack.pop();
        onStack.delete(w);
        comp.push(w);
      } while (w !== v);
      sccs.push(comp);
    }
  }

  for (const v in graph) {
    if (ids[v] === undefined) dfs(v);
  }
  return sccs;
}
      

Greedy Graph Coloring

Python

def greedy_color(graph):
    """Greedy graph coloring. Assigns smallest available color to each vertex.
    graph: dict of node -> set/list of neighbors.
    Returns dict of node -> color (0-indexed).
    Guarantees at most max_degree + 1 colors."""
    colors = {}
    for node in graph:
        # Gather colors used by already-colored neighbors
        used = {colors[n] for n in graph[node] if n in colors}
        # Assign smallest unused color
        color = 0
        while color in used:
            color += 1
        colors[node] = color
    return colors

def is_bipartite(graph):
    """Check if graph is 2-colorable using BFS.
    Equivalent to checking for odd-length cycles."""
    color = {}
    for start in graph:
        if start in color:
            continue
        # BFS from start
        queue = [start]
        color[start] = 0
        while queue:
            next_queue = []
            for node in queue:
                for neighbor in graph[node]:
                    if neighbor not in color:
                        color[neighbor] = 1 - color[node]
                        next_queue.append(neighbor)
                    elif color[neighbor] == color[node]:
                        return False  # odd cycle found
            queue = next_queue
    return True
      

Bridges & Articulation Points

Python

def find_bridges(graph):
    """Find all bridges (cut edges) in an undirected graph.
    A bridge is an edge whose removal disconnects the graph.
    graph: dict of node -> list of neighbors.
    Time: O(V + E)."""
    timer = [0]
    ids = {}
    low = {}
    bridges = []

    def dfs(v, parent):
        ids[v] = low[v] = timer[0]
        timer[0] += 1
        for w in graph.get(v, []):
            if w == parent:
                continue  # don't go back on the edge we came from
            if w not in ids:
                dfs(w, v)
                low[v] = min(low[v], low[w])
                # Bridge condition: w's subtree can't reach
                # anything at or above v
                if low[w] > ids[v]:
                    bridges.append((v, w))
            else:
                low[v] = min(low[v], ids[w])
    
    for v in graph:
        if v not in ids:
            dfs(v, -1)
    return bridges

def find_articulation_points(graph):
    """Find all articulation points (cut vertices).
    Time: O(V + E)."""
    timer = [0]
    ids = {}
    low = {}
    ap = set()

    def dfs(v, parent):
        ids[v] = low[v] = timer[0]
        timer[0] += 1
        children = 0
        for w in graph.get(v, []):
            if w == parent:
                continue
            if w not in ids:
                children += 1
                dfs(w, v)
                low[v] = min(low[v], low[w])
                # Non-root: AP if child can't escape above v
                if parent != -1 and low[w] >= ids[v]:
                    ap.add(v)
            else:
                low[v] = min(low[v], ids[w])
        # Root: AP if it has 2+ children in DFS tree
        if parent == -1 and children >= 2:
            ap.add(v)

    for v in graph:
        if v not in ids:
            dfs(v, -1)
    return ap
      

Hierholzer's Algorithm (Eulerian Path)

Python

from collections import defaultdict

def find_euler_path(graph):
    """Find Eulerian path/circuit in a directed graph.
    graph: dict of node -> list of neighbors (adjacency list).
    Returns the path as a list of vertices, or empty if none exists.
    
    For directed graphs:
    - Circuit: every node has in_degree == out_degree
    - Path: one node has out - in = 1 (start), one has in - out = 1 (end)"""
    # Convert to mutable adjacency list with index tracking
    adj = defaultdict(list)
    in_deg = defaultdict(int)
    out_deg = defaultdict(int)
    for u in graph:
        for v in graph[u]:
            adj[u].append(v)
            out_deg[u] += 1
            in_deg[v] += 1

    # Find start node
    start = None
    for node in set(list(in_deg.keys()) + list(out_deg.keys())):
        if out_deg[node] - in_deg[node] == 1:
            start = node
            break
    if start is None:
        # Eulerian circuit — start anywhere with edges
        for node in adj:
            if adj[node]:
                start = node
                break

    # Hierholzer's: DFS with edge deletion
    stack = [start]
    path = []
    edge_idx = defaultdict(int)  # tracks next unused edge for each node

    while stack:
        v = stack[-1]
        if edge_idx[v] < len(adj[v]):
            # Follow next unused edge
            w = adj[v][edge_idx[v]]
            edge_idx[v] += 1
            stack.append(w)
        else:
            # No more edges from v — add to path (in reverse)
            path.append(stack.pop())

    path.reverse()
    return path
      

Real-World Example: Detecting Circular Dependencies

In any package manager (npm, pip, cargo), circular dependencies between modules cause build failures. SCCs are the tool to detect and report them. If packages A, B, C form a cycle (A depends on B, B depends on C, C depends on A), they're in the same SCC. Any SCC with more than one node represents a circular dependency that needs to be broken.

Python

def detect_circular_deps(packages):
    """Detect circular dependencies in a package dependency graph.
    packages: dict of package_name -> [list of dependencies].
    Returns list of circular dependency groups (SCCs with size > 1)."""
    
    sccs = tarjan_scc(packages)  # using our Tarjan implementation
    
    circular = [scc for scc in sccs if len(scc) > 1]
    
    if circular:
        print("⚠️  Circular dependencies detected!")
        for i, group in enumerate(circular):
            print(f"  Cycle {i+1}: {' → '.join(group)} → {group[0]}")
    else:
        print("✅ No circular dependencies.")
    
    return circular

# Example: a real-world-ish package graph
packages = {
    'auth':     ['database', 'crypto'],
    'database': ['logger'],
    'logger':   ['config'],
    'config':   ['auth'],         # ← circular! auth → database → logger → config → auth
    'crypto':   [],
    'api':      ['auth', 'logger'],
    'frontend': ['api']
}

detect_circular_deps(packages)
# Output:
# ⚠️  Circular dependencies detected!
#   Cycle 1: auth → database → logger → config → auth

# The SCC condensation turns this into a DAG:
# {auth,database,logger,config} → crypto
# {auth,database,logger,config} ← api ← frontend
      

This exact approach is used by build tools. Webpack detects circular imports this way. Python's importlib warns about circular imports. Rust's cargo refuses to compile crates with cyclic dependencies. The underlying algorithm is always SCC detection.

Interview Patterns

💡 Bipartite = 2-Colorable A graph is bipartite if and only if it can be colored with just 2 colors — which means it has no odd-length cycles. Check bipartiteness with BFS/DFS: try to 2-color the graph, and if you hit a conflict (a node and its neighbor have the same color), it's not bipartite. This shows up directly in problems like "Is Graph Bipartite?" and "Possible Bipartition."
💡 SCCs for Dependency Analysis When a problem involves directed dependencies and asks about cycles or reachability, think SCCs. Condensing a graph to its SCC DAG simplifies many problems — once you have the DAG, topological sort and DP become available. Classic example: "what's the minimum number of nodes to reach all other nodes?" Answer: nodes with in-degree 0 in the SCC condensation.
💡 Euler vs. Hamilton: Know the Difference If the problem asks about visiting every edge, it's Eulerian (solvable in O(E)). If it asks about every vertex, it's Hamiltonian (NP-complete, use bitmask DP for small n). The word "path" alone doesn't tell you which — you need to check whether the constraint is on edges or vertices. "Reconstruct Itinerary" is Eulerian. "Shortest Superstring" involves Hamiltonian ideas.
💡 Bridges = Critical Connections The "Critical Connections in a Network" problem is literally asking for bridges. Use Tarjan's bridge-finding algorithm (DFS with low-link values). The key condition: edge (u, v) is a bridge if low[v] > ids[u]. This means v's subtree has no back edge reaching above u — removing (u, v) disconnects v's subtree from the rest.
💡 SCC + Topological Sort for 2-SAT 2-SAT (2-satisfiability) is solved by: (1) build the implication graph (each clause a∨b becomes ¬a→b and ¬b→a), (2) find SCCs, (3) check if any variable and its negation are in the same SCC (if so, unsatisfiable), (4) assign values using reverse topological order of the SCC DAG. This runs in O(V + E) and is a favorite in competitive programming.

Practice Problems

#ProblemDifficultyKey Concept
785Is Graph Bipartite?Medium2-coloring / BFS
332Reconstruct ItineraryHardEulerian path (Hierholzer's)
1192Critical Connections in a NetworkHardTarjan's bridges
886Possible BipartitionMediumGraph coloring / bipartite
943Find the Shortest SuperstringHardHamiltonian path + bitmask DP
2360Longest Cycle in a GraphHardCycle detection / functional graph
2101Detonate the Maximum BombsMediumDirected reachability / SCC ideas
1557Min Vertices to Reach All NodesMediumIn-degree 0 in DAG (SCC condensation)