DSA DAY ONE
DSA DAY ONE
PART I — FOUNDATIONS
Chapter 1: C++ Essentials for DSA
Chapter 2: C++ STL Deep Dive
Chapter 3: Complexity Analysis
PART II — LINEAR DATA STRUCTURES
Chapter 4: Arrays & Strings
Chapter 5: Two Pointers & Sliding Window
Chapter 6: Hashing
Chapter 7: Linked Lists
Chapter 8: Stacks
Chapter 9: Queues & Deques
PART III — TREES
Chapter 10: Binary Trees
Chapter 11: Binary Search Trees (BST)
Chapter 12: Heaps & Priority Queues
Chapter 13: Tries (Prefix Trees)
Chapter 14: Segment Tree & Fenwick Tree
PART IV — ALGORITHMS
Chapter 15: Sorting Algorithms
Chapter 16: Binary Search
Chapter 17: Recursion & Backtracking
Chapter 18: Graphs — Fundamentals & Traversals
Graph Theory — Core Vocabulary & PropertiesRepresentations — Adjacency List, Matrix & Edge ListBFS — Breadth-First SearchDFS — Depth-First SearchCycle Detection — Undirected & DirectedTopological Sort — Kahn's & DFS-BasedConnected ComponentsStrongly Connected Components — Kosaraju's AlgorithmBipartite Graph CheckMulti-Source BFS & 0-1 BFSSample ProblemsLeetCode Problem Set
Chapter 19: Graph Algorithms — Shortest Paths, MST & Union-Find
Chapter 20: Dynamic Programming
Chapter 21: Greedy Algorithms
Chapter 22: Divide & Conquer
PART V — COMPETITIVE & MATH
Chapter 23: Bit Manipulation
Chapter 24: Math & Number Theory
PART VI — PATTERNS
Chapter 25: Pattern Recognition Mastery
PART VII — SYSTEM DESIGN
Chapter 26: System Design for FAANG Interviews
PART VIII — INTERVIEW STRATEGY
Chapter 27: Cracking FAANG — Strategy & Communication
18

Graphs — Fundamentals & Traversals


Chapter 18 ·  Part 4: ALGORITHMS ·  Est. 25 min read

Chapter Goal: Build a comprehensive, interview-ready command of graph theory and algorithms. Graphs are the most versatile data structure — every network, schedule, map, and dependency problem reduces to one. Master the representations, traversal templates, cycle detection, topological sort, SCC, and bipartite check, and you unlock solutions to an enormous family of FAANG problems.


18.1 Graph Theory — Core Vocabulary & Properties

Definitions

G = (V, E)
  V = set of vertices (nodes)
  E = set of edges (connections between vertices)

Undirected edge: (u, v) = (v, u)  — connection is bidirectional
Directed edge:   (u, v) ≠ (v, u) — connection goes only from u to v

Weighted:   each edge has a weight/cost
Unweighted: all edges are equal (or weight = 1)

Simple graph:     no self-loops, no multi-edges
Multi-graph:      allows multiple edges between same pair

Degree of vertex v (undirected): number of edges incident to v
In-degree  (directed): edges coming INTO v
Out-degree (directed): edges going OUT of v

Special Graph Types

Type Definition Key Property
Tree Connected, n vertices, n-1 edges, no cycle Unique path between any two nodes
Forest Acyclic undirected graph (collection of trees)
DAG Directed Acyclic Graph Topological sort exists
Bipartite 2-colorable (no odd cycles) BFS check works
Complete Every pair connected n(n-1)/2 edges
Sparse E ≪ V² Use adjacency list
Dense E ≈ V² Adjacency matrix may be ok

Graph Terminology

Path:        Sequence of vertices connected by edges
Simple path: Path with no repeated vertices
Cycle:       Path where start == end (length ≥ 3 in simple graph)
Connected:   Every pair of vertices has a path between them (undirected)
Strongly connected: Every pair has a directed path in BOTH directions
Weakly connected: Connected when direction is ignored
Component:   Maximal connected subgraph

18.2 Representations — Adjacency List, Matrix & Edge List

Adjacency List — Default Choice

int n = 6;  // Number of vertices (0-indexed: 0..n-1)
 
// Unweighted undirected graph
vector<vector<int>> adj(n);
adj[0].push_back(1); adj[1].push_back(0);  // Edge 0-1 (both directions)
adj[0].push_back(2); adj[2].push_back(0);
adj[1].push_back(3); adj[3].push_back(1);
 
// Weighted directed graph
vector<vector<pair<int,int>>> wadj(n);   // {neighbor, weight}
wadj[0].push_back({1, 5});   // Edge 0→1 with weight 5
wadj[0].push_back({2, 3});   // Edge 0→2 with weight 3
wadj[1].push_back({3, 2});   // Edge 1→3 with weight 2
 
// Build from edge list input
int edges; cin >> n >> edges;
vector<vector<int>> g(n);
for (int i = 0; i < edges; i++) {
    int u, v; cin >> u >> v;
    g[u].push_back(v);
    g[v].push_back(u);  // Remove for directed graph
}

Complexity:

  • Space: O(V + E)
  • Check edge (u,v): O(degree(u))
  • Iterate all neighbors of u: O(degree(u))

Adjacency Matrix

int n = 6;
vector<vector<int>> mat(n, vector<int>(n, 0));
 
mat[0][1] = mat[1][0] = 1;   // Unweighted undirected
mat[0][1] = 5;                // Directed weighted: 0→1 with weight 5
 
// Check if edge exists: O(1)
if (mat[u][v]) { /* edge exists */ }
 
// All neighbors of u: O(n) scan
for (int v = 0; v < n; v++)
    if (mat[u][v]) process(u, v);

Complexity:

  • Space: O(V²) — prohibitive for large sparse graphs
  • Check edge (u,v): O(1) ← key advantage
  • Iterate neighbors: O(V) — inefficient for sparse graphs

Edge List

// Unweighted
vector<pair<int,int>> edges = {{0,1},{0,2},{1,3},{2,3},{3,4}};
 
// Weighted (for Kruskal's MST)
vector<tuple<int,int,int>> wedges;  // {weight, u, v}
wedges.push_back({5, 0, 1});
wedges.push_back({3, 0, 2});
sort(wedges.begin(), wedges.end());  // Sort by weight

Use edge list for: Kruskal's MST, when you need to sort edges by weight.

Choosing a Representation

Factor Adjacency List Adjacency Matrix Edge List
Space O(V + E) O(V²) O(E)
Add edge O(1) O(1) O(1)
Edge check O(degree) O(1) O(E)
Iterate neighbors O(degree) O(V) O(E)
Best for Sparse graphs (most problems) Dense graphs, O(1) edge check Kruskal's MST

18.3 BFS — Breadth-First Search

BFS explores vertices layer by layer. It guarantees the shortest path (in terms of edge count) in unweighted graphs.

Standard BFS Template

vector<int> bfs(int start, int n, vector<vector<int>>& adj) {
    vector<int> dist(n, -1);    // -1 = unvisited
    queue<int> q;
 
    dist[start] = 0;
    q.push(start);              // Mark visited WHEN ENQUEUING (not dequeuing!)
 
    while (!q.empty()) {
        int u = q.front(); q.pop();
 
        for (int v : adj[u]) {
            if (dist[v] == -1) {       // Not yet visited
                dist[v] = dist[u] + 1;
                q.push(v);             // Mark here
            }
        }
    }
    return dist;
}
// Time: O(V + E), Space: O(V)
// dist[v] = shortest path from start to v (-1 if unreachable)

Level-Order BFS (Process Layer by Layer)

void bfsLayers(int start, vector<vector<int>>& adj) {
    vector<bool> visited(adj.size(), false);
    queue<int> q;
    q.push(start);
    visited[start] = true;
    int level = 0;
 
    while (!q.empty()) {
        int sz = q.size();          // Snapshot: all nodes at current level
        for (int i = 0; i < sz; i++) {
            int u = q.front(); q.pop();
            process(u, level);
            for (int v : adj[u])
                if (!visited[v]) { visited[v] = true; q.push(v); }
        }
        level++;
    }
}

BFS on a Grid

// Shortest path in a grid from src to dst
// 0 = open, 1 = blocked
int bfsGrid(vector<vector<int>>& grid, pair<int,int> src, pair<int,int> dst) {
    int R = grid.size(), C = grid[0].size();
    vector<vector<int>> dist(R, vector<int>(C, -1));
    int dx[] = {0,0,1,-1}, dy[] = {1,-1,0,0};
    queue<pair<int,int>> q;
 
    auto [sr, sc] = src;
    dist[sr][sc] = 0;
    q.push(src);
 
    while (!q.empty()) {
        auto [r, c] = q.front(); q.pop();
        if (r == dst.first && c == dst.second) return dist[r][c];
 
        for (int d = 0; d < 4; d++) {
            int nr = r+dx[d], nc = c+dy[d];
            if (nr>=0 && nr<R && nc>=0 && nc<C
                && grid[nr][nc]==0 && dist[nr][nc]==-1) {
                dist[nr][nc] = dist[r][c] + 1;
                q.push({nr, nc});
            }
        }
    }
    return -1;
}
// Time: O(R × C), Space: O(R × C)

18.4 DFS — Depth-First Search

DFS explores as deep as possible along each branch before backtracking. It naturally produces a DFS tree and timestamps useful for many algorithms.

Recursive DFS Template

vector<bool> visited;
vector<vector<int>> adj;
 
void dfs(int u) {
    visited[u] = true;
    process(u);                     // Pre-order processing
 
    for (int v : adj[u]) {
        if (!visited[v]) {
            dfs(v);
        }
    }
    // Post-order processing here (used in topological sort, SCC)
}
 
// Run from every unvisited node (for disconnected graphs)
void dfsAll(int n) {
    visited.assign(n, false);
    for (int i = 0; i < n; i++)
        if (!visited[i]) dfs(i);
}
// Time: O(V + E), Space: O(V) — call stack depth ≤ V

DFS with Discovery & Finish Times

int timer = 0;
vector<int> disc, finish;  // disc[u] = discovery time, finish[u] = finish time
 
void dfs(int u) {
    disc[u] = ++timer;   // Discovered
    visited[u] = true;
 
    for (int v : adj[u]) {
        if (!visited[v]) dfs(v);
    }
 
    finish[u] = ++timer; // Finished (all neighbors processed)
}
// disc[u] < disc[v] < finish[v] < finish[u] → v is in DFS subtree of u
// Used in: SCC (Kosaraju's), bridge/articulation point finding

Iterative DFS (Stack-Based)

void dfsIterative(int start, vector<vector<int>>& adj) {
    int n = adj.size();
    vector<bool> visited(n, false);
    stack<int> st;
 
    st.push(start);
    while (!st.empty()) {
        int u = st.top(); st.pop();
        if (visited[u]) continue;
        visited[u] = true;
        process(u);
 
        // Push neighbors in reverse order to process them in original order
        for (int i = adj[u].size()-1; i >= 0; i--)
            if (!visited[adj[u][i]]) st.push(adj[u][i]);
    }
}
// Note: iterative DFS may visit nodes in different order than recursive DFS
// The key difference: recursive pushes implicitly (call stack), iterative explicitly

BFS vs. DFS — Decision Guide

Use Case BFS DFS
Shortest path (unweighted) Guaranteed Not guaranteed
Level-order / min steps Natural Needs extra work
Topological sort Kahn's (BFS) Post-order DFS
Cycle detection Works Works
Connected components Works Works
SCC (directed) Needs Kosaraju Tarjan's or Kosaraju's DFS
Bipartite check Natural Works
Finding all paths Less natural Natural
Memory (wide graphs) Warning: O(width) queue O(depth) stack
Memory (deep graphs) O(width) Warning: Stack overflow risk

18.5 Cycle Detection — Undirected & Directed

Undirected Graphs — Parent Tracking

In an undirected DFS, every edge (u, v) is seen twice. A cycle exists when we reach an already-visited neighbor that is not our parent in the DFS tree.

bool hasCycleUndirected(int u, int parent, vector<bool>& visited,
                         vector<vector<int>>& adj) {
    visited[u] = true;
    for (int v : adj[u]) {
        if (!visited[v]) {
            if (hasCycleUndirected(v, u, visited, adj)) return true;
        } else if (v != parent) {
            return true;    // Visited neighbor that's not our parent → cycle!
        }
    }
    return false;
}
 
bool detectCycleUndirected(int n, vector<vector<int>>& adj) {
    vector<bool> visited(n, false);
    for (int i = 0; i < n; i++)
        if (!visited[i])
            if (hasCycleUndirected(i, -1, visited, adj)) return true;
    return false;
}
// Time: O(V + E), Space: O(V)
 
// Warning: Caveat: parent tracking doesn't work for multi-edges (parallel edges)
// Use edge index or DSU for multi-graphs

Directed Graphs — Three-Color DFS

In a directed graph, a cycle exists when DFS reaches a vertex that is currently on the DFS path (gray — currently being processed, not yet finished).

Color convention:
  0 = WHITE: unvisited
  1 = GRAY:  in current DFS path (on the call stack)
  2 = BLACK: fully processed (all descendants visited)

Cycle found when: we encounter a GRAY neighbor (back edge)
// Returns true if cycle detected
bool dfsDirected(int u, vector<int>& color, vector<vector<int>>& adj) {
    color[u] = 1;   // GRAY: mark as in-progress
 
    for (int v : adj[u]) {
        if (color[v] == 1) return true;    // GRAY → back edge → CYCLE!
        if (color[v] == 0)                 // WHITE → unvisited → recurse
            if (dfsDirected(v, color, adj)) return true;
        // BLACK → already fully processed → safe (cross/forward edge)
    }
 
    color[u] = 2;   // BLACK: fully processed
    return false;
}
 
bool hasCycleDirected(int n, vector<vector<int>>& adj) {
    vector<int> color(n, 0);   // All WHITE initially
    for (int i = 0; i < n; i++)
        if (color[i] == 0)
            if (dfsDirected(i, color, adj)) return true;
    return false;
}
// Time: O(V + E), Space: O(V)

18.6 Topological Sort — Kahn's & DFS-Based

Topological Sort: A linear ordering of vertices in a DAG such that for every directed edge (u, v), u comes before v. Only valid for DAGs.

Kahn's Algorithm (BFS-Based) — Preferred in Interviews

Algorithm:
1. Compute in-degree of every vertex
2. Enqueue all vertices with in-degree 0 (no prerequisites)
3. While queue not empty:
   a. Dequeue vertex u, add to result
   b. For each neighbor v of u: decrease in-degree[v] by 1
      If in-degree[v] == 0: enqueue v
4. If result.size() < n: cycle exists (some vertices never reached 0)
vector<int> topoSort_Kahn(int n, vector<vector<int>>& adj) {
    vector<int> inDegree(n, 0);
    for (int u = 0; u < n; u++)
        for (int v : adj[u]) inDegree[v]++;
 
    queue<int> q;
    for (int i = 0; i < n; i++)
        if (inDegree[i] == 0) q.push(i);
 
    vector<int> order;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        order.push_back(u);
        for (int v : adj[u]) {
            if (--inDegree[v] == 0) q.push(v);
        }
    }
 
    if ((int)order.size() != n) return {};  // Cycle detected!
    return order;
}
// Time: O(V + E), Space: O(V)
 
/*
  Graph: 0→1→3, 0→2→3, 2→4
  in-degrees: [0, 1, 1, 2, 1]
  Queue: [0]
  Process 0: order=[0], decrease 1 and 2's in-degree → queue=[1,2]
  Process 1: order=[0,1], decrease 3's in-degree (now 1) → queue=[2]
  Process 2: order=[0,1,2], decrease 3(now 0) and 4(now 0) → queue=[3,4]
  Process 3: order=[0,1,2,3] → queue=[4]
  Process 4: order=[0,1,2,3,4]
  Valid topological order!
*/

DFS-Based Topological Sort

void dfsTopoSort(int u, vector<bool>& visited, stack<int>& st,
                 vector<vector<int>>& adj) {
    visited[u] = true;
    for (int v : adj[u])
        if (!visited[v]) dfsTopoSort(v, visited, st, adj);
    st.push(u);   // Push AFTER all neighbors processed (post-order)
}
 
vector<int> topoSort_DFS(int n, vector<vector<int>>& adj) {
    vector<bool> visited(n, false);
    stack<int> st;
 
    for (int i = 0; i < n; i++)
        if (!visited[i]) dfsTopoSort(i, visited, st, adj);
 
    vector<int> order;
    while (!st.empty()) { order.push_back(st.top()); st.pop(); }
    return order;
}
// Time: O(V + E), Space: O(V)
// Note: doesn't detect cycles directly — use color-based DFS (18.5) for that

Application: Course Schedule (Prerequisites)

// LC 207: Can you finish all courses given prerequisites?
// = Detect cycle in directed graph of prerequisites
 
bool canFinish(int n, vector<vector<int>>& prerequisites) {
    vector<vector<int>> adj(n);
    for (auto& p : prerequisites) adj[p[1]].push_back(p[0]);
 
    vector<int> inDeg(n, 0);
    for (int u = 0; u < n; u++) for (int v : adj[u]) inDeg[v]++;
 
    queue<int> q;
    for (int i = 0; i < n; i++) if (inDeg[i] == 0) q.push(i);
 
    int processed = 0;
    while (!q.empty()) {
        int u = q.front(); q.pop(); processed++;
        for (int v : adj[u]) if (--inDeg[v] == 0) q.push(v);
    }
    return processed == n;  // All processed → no cycle → can finish
}

18.7 Connected Components

Undirected Graph — Count Components via DFS/BFS

int countComponents(int n, vector<vector<int>>& adj) {
    vector<bool> visited(n, false);
    int components = 0;
 
    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            components++;
            // BFS from i to visit entire component
            queue<int> q;
            q.push(i); visited[i] = true;
            while (!q.empty()) {
                int u = q.front(); q.pop();
                for (int v : adj[u])
                    if (!visited[v]) { visited[v] = true; q.push(v); }
            }
        }
    }
    return components;
}
// Time: O(V + E), Space: O(V)

Label Each Component (Useful for Component Membership Queries)

vector<int> componentOf(int n, vector<vector<int>>& adj) {
    vector<int> comp(n, -1);
    int compId = 0;
 
    for (int i = 0; i < n; i++) {
        if (comp[i] != -1) continue;
        // BFS from i
        queue<int> q;
        q.push(i); comp[i] = compId;
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int v : adj[u])
                if (comp[v] == -1) { comp[v] = compId; q.push(v); }
        }
        compId++;
    }
    return comp;   // comp[v] = component ID of vertex v
}

DFS Flood Fill on Grid

// Count connected regions of 1s in a grid (Number of Islands)
int numIslands(vector<vector<char>>& grid) {
    int R = grid.size(), C = grid[0].size(), count = 0;
 
    function<void(int,int)> dfs = [&](int r, int c) {
        if (r<0||r>=R||c<0||c>=C||grid[r][c]!='1') return;
        grid[r][c] = '0';    // Mark visited in-place
        dfs(r+1,c); dfs(r-1,c); dfs(r,c+1); dfs(r,c-1);
    };
 
    for (int r = 0; r < R; r++)
        for (int c = 0; c < C; c++)
            if (grid[r][c] == '1') { count++; dfs(r, c); }
    return count;
}
// Time: O(R × C), Space: O(R × C) recursion stack worst case

18.8 Strongly Connected Components — Kosaraju's Algorithm

An SCC is a maximal set of vertices such that every vertex is reachable from every other vertex via directed paths.

Kosaraju's Algorithm — Two-Pass DFS

Step 1: DFS on original graph, track FINISH order (push to stack when done)
Step 2: Transpose (reverse) all edges
Step 3: DFS on transposed graph in REVERSE finish order (pop from stack)
        Each DFS tree in Step 3 = one SCC
Intuition: In the original graph, finish-order DFS visits "source" SCCs last
(they have no outgoing edges to other SCCs). In the transposed graph, starting
from these source SCCs means we can only reach within that SCC.
vector<vector<int>> kosaraju(int n, vector<vector<int>>& adj) {
    // Step 1: DFS to get finish order
    vector<bool> visited(n, false);
    stack<int> finishOrder;
 
    function<void(int)> dfs1 = [&](int u) {
        visited[u] = true;
        for (int v : adj[u]) if (!visited[v]) dfs1(v);
        finishOrder.push(u);   // Push AFTER all descendants
    };
    for (int i = 0; i < n; i++) if (!visited[i]) dfs1(i);
 
    // Step 2: Build transposed graph
    vector<vector<int>> radj(n);
    for (int u = 0; u < n; u++)
        for (int v : adj[u]) radj[v].push_back(u);
 
    // Step 3: DFS on transposed in reverse finish order
    fill(visited.begin(), visited.end(), false);
    vector<vector<int>> sccs;
 
    function<void(int, vector<int>&)> dfs2 = [&](int u, vector<int>& scc) {
        visited[u] = true;
        scc.push_back(u);
        for (int v : radj[u]) if (!visited[v]) dfs2(v, scc);
    };
 
    while (!finishOrder.empty()) {
        int u = finishOrder.top(); finishOrder.pop();
        if (!visited[u]) {
            sccs.push_back({});
            dfs2(u, sccs.back());
        }
    }
    return sccs;
}
// Time: O(V + E), Space: O(V + E) for transposed graph

18.9 Bipartite Graph Check

A graph is bipartite if its vertices can be colored with 2 colors such that no two adjacent vertices share the same color. Equivalently: it contains no odd-length cycles.

BFS 2-Coloring

bool isBipartite(int n, vector<vector<int>>& adj) {
    vector<int> color(n, -1);   // -1 = uncolored
 
    for (int start = 0; start < n; start++) {
        if (color[start] != -1) continue;
 
        queue<int> q;
        q.push(start);
        color[start] = 0;
 
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int v : adj[u]) {
                if (color[v] == -1) {
                    color[v] = 1 - color[u];   // Assign opposite color
                    q.push(v);
                } else if (color[v] == color[u]) {
                    return false;   // Same color → not bipartite!
                }
            }
        }
    }
    return true;
}
// Time: O(V + E), Space: O(V)

DFS 2-Coloring

bool dfsColor(int u, int c, vector<int>& color, vector<vector<int>>& adj) {
    color[u] = c;
    for (int v : adj[u]) {
        if (color[v] == -1) {
            if (!dfsColor(v, 1-c, color, adj)) return false;
        } else if (color[v] == c) {
            return false;   // Same color as current → not bipartite
        }
    }
    return true;
}
 
bool isBipartiteDFS(int n, vector<vector<int>>& adj) {
    vector<int> color(n, -1);
    for (int i = 0; i < n; i++)
        if (color[i] == -1)
            if (!dfsColor(i, 0, color, adj)) return false;
    return true;
}

18.10 Multi-Source BFS & 0-1 BFS

Multi-Source BFS

Start BFS from ALL source nodes simultaneously. Each source starts at distance 0. The BFS naturally propagates the minimum distance from any source.

// Find minimum distance from any source (set of sources) to each node
vector<int> multiSourceBFS(int n, vector<vector<int>>& adj,
                            vector<int>& sources) {
    vector<int> dist(n, -1);
    queue<int> q;
 
    for (int s : sources) {
        dist[s] = 0;
        q.push(s);
    }
 
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u]) {
            if (dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
    return dist;
}
// Applications: 01-Matrix (distance to nearest 0), Rotting Oranges,
//               Walls and Gates, spreading fire/infection problems

0-1 BFS (Deque-Based for Binary-Weight Edges)

When edge weights are 0 or 1, use a deque instead of a priority queue. Achieves O(V + E) instead of O((V+E) log V).

vector<int> zeroOneBFS(int start, int n,
                        vector<vector<pair<int,int>>>& adj) {
    // adj[u] = {(v, weight)} where weight ∈ {0, 1}
    vector<int> dist(n, INT_MAX);
    deque<int> dq;
 
    dist[start] = 0;
    dq.push_front(start);
 
    while (!dq.empty()) {
        int u = dq.front(); dq.pop_front();
        for (auto [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                if (w == 0) dq.push_front(v);   // 0-weight: higher priority
                else        dq.push_back(v);     // 1-weight: lower priority
            }
        }
    }
    return dist;
}
// Time: O(V + E), Space: O(V)

18.11 Sample Problems


Problem 1 — Number of Islands (Medium) — LC 200

int numIslands(vector<vector<char>>& grid) {
    int R=grid.size(), C=grid[0].size(), count=0;
    function<void(int,int)> dfs=[&](int r,int c) {
        if(r<0||r>=R||c<0||c>=C||grid[r][c]!='1') return;
        grid[r][c]='0';
        dfs(r+1,c);dfs(r-1,c);dfs(r,c+1);dfs(r,c-1);
    };
    for(int r=0;r<R;r++) for(int c=0;c<C;c++)
        if(grid[r][c]=='1'){count++;dfs(r,c);}
    return count;
}
// Time: O(R×C), Space: O(R×C) recursion

Problem 2 — Number of Provinces (Medium) — LC 547

Given an n×n adjacency matrix, find the number of connected components.

int findCircleNum(vector<vector<int>>& isConnected) {
    int n=isConnected.size(), count=0;
    vector<bool> visited(n,false);
    function<void(int)> dfs=[&](int u) {
        visited[u]=true;
        for(int v=0;v<n;v++) if(!visited[v]&&isConnected[u][v]) dfs(v);
    };
    for(int i=0;i<n;i++) if(!visited[i]){count++;dfs(i);}
    return count;
}
// Time: O(n²), Space: O(n)

Problem 3 — Course Schedule (Medium) — LC 207

bool canFinish(int n, vector<vector<int>>& prerequisites) {
    vector<vector<int>> adj(n);
    vector<int> inDeg(n,0);
    for(auto& p:prerequisites){adj[p[1]].push_back(p[0]);inDeg[p[0]]++;}
    queue<int> q;
    for(int i=0;i<n;i++) if(inDeg[i]==0) q.push(i);
    int cnt=0;
    while(!q.empty()){
        int u=q.front();q.pop();cnt++;
        for(int v:adj[u]) if(--inDeg[v]==0) q.push(v);
    }
    return cnt==n;
}
// Time: O(V+E), Space: O(V+E)

Problem 4 — Course Schedule II (Medium) — LC 210

vector<int> findOrder(int n, vector<vector<int>>& prerequisites) {
    vector<vector<int>> adj(n);
    vector<int> inDeg(n,0);
    for(auto& p:prerequisites){adj[p[1]].push_back(p[0]);inDeg[p[0]]++;}
    queue<int> q;
    for(int i=0;i<n;i++) if(inDeg[i]==0) q.push(i);
    vector<int> order;
    while(!q.empty()){
        int u=q.front();q.pop();order.push_back(u);
        for(int v:adj[u]) if(--inDeg[v]==0) q.push(v);
    }
    return (int)order.size()==n ? order : vector<int>{};
}
// Time: O(V+E), Space: O(V+E)

Problem 5 — Clone Graph (Medium) — LC 133

Node* cloneGraph(Node* node) {
    if(!node) return nullptr;
    unordered_map<Node*,Node*> cloned;
    queue<Node*> q;
    q.push(node);
    cloned[node] = new Node(node->val);
 
    while(!q.empty()){
        Node* u=q.front();q.pop();
        for(Node* nb:u->neighbors){
            if(!cloned.count(nb)){
                cloned[nb]=new Node(nb->val);
                q.push(nb);
            }
            cloned[u]->neighbors.push_back(cloned[nb]);
        }
    }
    return cloned[node];
}
// Time: O(V+E), Space: O(V)

Problem 6 — Is Graph Bipartite? (Medium) — LC 785

bool isBipartite(vector<vector<int>>& graph) {
    int n=graph.size();
    vector<int> color(n,-1);
    for(int start=0;start<n;start++){
        if(color[start]!=-1) continue;
        queue<int> q;
        q.push(start); color[start]=0;
        while(!q.empty()){
            int u=q.front();q.pop();
            for(int v:graph[u]){
                if(color[v]==-1){color[v]=1-color[u];q.push(v);}
                else if(color[v]==color[u]) return false;
            }
        }
    }
    return true;
}
// Time: O(V+E), Space: O(V)

Problem 7 — Pacific Atlantic Water Flow (Medium) — LC 417

Find cells from which water can flow to BOTH the Pacific and Atlantic oceans. Water flows from higher/equal to lower/equal height cells.

vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
    int R=heights.size(), C=heights[0].size();
    vector<vector<bool>> pac(R,vector<bool>(C,false));
    vector<vector<bool>> atl(R,vector<bool>(C,false));
    int dx[]={0,0,1,-1}, dy[]={1,-1,0,0};
 
    // BFS from ocean boundaries INWARD (reverse flow)
    auto bfs=[&](queue<pair<int,int>>& q, vector<vector<bool>>& reach) {
        while(!q.empty()){
            auto[r,c]=q.front();q.pop();
            for(int d=0;d<4;d++){
                int nr=r+dx[d],nc=c+dy[d];
                if(nr>=0&&nr<R&&nc>=0&&nc<C&&!reach[nr][nc]
                   &&heights[nr][nc]>=heights[r][c]){
                    reach[nr][nc]=true;
                    q.push({nr,nc});
                }
            }
        }
    };
 
    queue<pair<int,int>> pq, aq;
    for(int r=0;r<R;r++){
        pac[r][0]=true; pq.push({r,0});
        atl[r][C-1]=true; aq.push({r,C-1});
    }
    for(int c=0;c<C;c++){
        pac[0][c]=true; pq.push({0,c});
        atl[R-1][c]=true; aq.push({R-1,c});
    }
    bfs(pq,pac); bfs(aq,atl);
 
    vector<vector<int>> result;
    for(int r=0;r<R;r++)
        for(int c=0;c<C;c++)
            if(pac[r][c]&&atl[r][c]) result.push_back({r,c});
    return result;
}
// Time: O(R×C), Space: O(R×C)

Problem 8 — All Paths From Source to Target (Medium) — LC 797

Find all paths from node 0 to node n-1 in a DAG.

vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
    int n=graph.size();
    vector<vector<int>> result;
    vector<int> path={0};
    function<void(int)> dfs=[&](int u){
        if(u==n-1){result.push_back(path);return;}
        for(int v:graph[u]){
            path.push_back(v);
            dfs(v);
            path.pop_back();
        }
    };
    dfs(0);
    return result;
}
// Time: O(2^n × n) — exponential paths possible
// Space: O(n) depth

Problem 9 — Find Eventual Safe States (Medium) — LC 802

Find all nodes from which every path leads to a terminal node (no cycle).

// Approach: Reverse graph + Kahn's topological sort
// Safe nodes = nodes that reach terminal without cycle = nodes with out-degree 0
// in the original = nodes with in-degree 0 in the REVERSED graph
vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
    int n=graph.size();
    vector<vector<int>> radj(n);
    vector<int> outDeg(n,0);
 
    for(int u=0;u<n;u++){
        outDeg[u]=graph[u].size();
        for(int v:graph[u]) radj[v].push_back(u);
    }
 
    queue<int> q;
    for(int i=0;i<n;i++) if(outDeg[i]==0) q.push(i);
 
    vector<bool> safe(n,false);
    while(!q.empty()){
        int u=q.front();q.pop();
        safe[u]=true;
        for(int v:radj[u])
            if(--outDeg[v]==0) q.push(v);
    }
 
    vector<int> result;
    for(int i=0;i<n;i++) if(safe[i]) result.push_back(i);
    return result;
}
// Time: O(V+E), Space: O(V+E)

Problem 10 — Minimum Height Trees (Medium) — LC 310

Find all roots that minimize the height of the tree formed by an undirected tree.

Key insight: The answer always has ≤ 2 nodes — the center(s) of the longest path. Repeatedly remove leaves (degree 1) until ≤ 2 nodes remain.

vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
    if(n==1) return {0};
    vector<vector<int>> adj(n);
    vector<int> deg(n,0);
    for(auto& e:edges){
        adj[e[0]].push_back(e[1]);
        adj[e[1]].push_back(e[0]);
        deg[e[0]]++; deg[e[1]]++;
    }
 
    queue<int> leaves;
    for(int i=0;i<n;i++) if(deg[i]==1) leaves.push(i);
 
    int remaining=n;
    while(remaining>2){
        int sz=leaves.size();
        remaining-=sz;
        for(int i=0;i<sz;i++){
            int leaf=leaves.front();leaves.pop();
            for(int nb:adj[leaf])
                if(--deg[nb]==1) leaves.push(nb);
        }
    }
    vector<int> result;
    while(!leaves.empty()){result.push_back(leaves.front());leaves.pop();}
    return result;
}
// Time: O(V), Space: O(V)

Problem 11 — Alien Dictionary (Hard) — LC 269

Given a list of words in an alien language sorted lexicographically, determine the order of characters.

string alienOrder(vector<string>& words) {
    unordered_map<char,unordered_set<char>> adj;
    unordered_map<char,int> inDeg;
 
    // Initialize all characters with in-degree 0
    for(auto& w:words) for(char c:w) inDeg[c]=0;
 
    // Extract ordering from adjacent word pairs
    for(int i=0;i<(int)words.size()-1;i++){
        string& a=words[i], &b=words[i+1];
        int len=min(a.size(),b.size());
        bool found=false;
        for(int j=0;j<(int)len;j++){
            if(a[j]!=b[j]){
                if(!adj[a[j]].count(b[j])){
                    adj[a[j]].insert(b[j]);
                    inDeg[b[j]]++;
                }
                found=true; break;
            }
        }
        // Invalid: word a is prefix of b but longer → impossible
        if(!found && a.size()>b.size()) return "";
    }
 
    // Kahn's topological sort
    queue<char> q;
    for(auto& [c,d]:inDeg) if(d==0) q.push(c);
 
    string order;
    while(!q.empty()){
        char c=q.front();q.pop();
        order+=c;
        for(char nb:adj[c])
            if(--inDeg[nb]==0) q.push(nb);
    }
    return order.size()==inDeg.size() ? order : "";
}
// Time: O(C) where C = total characters, Space: O(U + min(U², N)) where U=unique chars

Problem 12 — Reconstruct Itinerary (Hard) — LC 332

Given a list of airline tickets, reconstruct the itinerary starting from JFK. Use all tickets exactly once; choose lexicographically smallest valid itinerary.

// Hierholzer's algorithm for Eulerian path
vector<string> findItinerary(vector<vector<string>>& tickets) {
    unordered_map<string, priority_queue<string,
                  vector<string>, greater<string>>> adj;
 
    for(auto& t:tickets) adj[t[0]].push(t[1]);
 
    vector<string> result;
    stack<string> st;
    st.push("JFK");
 
    while(!st.empty()){
        string u=st.top();
        if(!adj[u].empty()){
            st.push(adj[u].top());
            adj[u].pop();
        } else {
            result.push_back(u);
            st.pop();
        }
    }
    reverse(result.begin(),result.end());
    return result;
}
// Time: O(E log E) — priority queue for lexicographic order
// Space: O(V + E)

18.12 LeetCode Problem Set

# Problem Difficulty Core Technique Link
1 Number of Islands Medium DFS flood fill (connected components) LC 200
2 Course Schedule Medium Topological sort / cycle detection LC 207
3 Course Schedule II Medium Kahn's topological sort (order) LC 210
4 Is Graph Bipartite? Medium BFS 2-coloring LC 785
5 Pacific Atlantic Water Flow Medium Multi-source BFS/DFS from boundaries LC 417
6 All Paths From Source to Target Medium DFS all paths in DAG LC 797
7 Alien Dictionary Hard Topological sort from string comparisons LC 269
8 Reconstruct Itinerary Hard Hierholzer's Eulerian path + priority queue LC 332

Suggested order: #1 (Islands) — DFS connected components. #2 and #3 (Course Schedule I and II) — the canonical topological sort problems. #4 (Bipartite) — BFS 2-coloring. #5 (Pacific Atlantic) — elegant reverse multi-source BFS. #6 (All Paths) — DFS backtracking on DAG. #7 (Alien Dictionary) — builds topological sort from string constraints — the most complex problem here. #8 (Itinerary) — Eulerian path with Hierholzer's algorithm.


← PreviousChapter 17: Recursion & BacktrackingNext →Chapter 19: Graph Algorithms — Shortest Paths, MST & Union-Find
Chapter 18 — Progress
0 / 26 COMPLETE
Graph Representations
  • Build an adjacency list for undirected and directed weighted graphs
  • Know complexity: adj list O(V+E) space, matrix O(V²) space, edge list O(E) space
  • Choose representation: sparse → adj list; dense or O(1) edge check → matrix; Kruskal's → edge list
BFS
  • Write BFS template with `dist[]` initialized to -1 (mark visited WHEN ENQUEUING)
  • Write level-order BFS using `int sz = q.size()` snapshot
  • Apply multi-source BFS by initializing all sources in queue at distance 0
DFS
  • Write recursive DFS with visited array
  • Write iterative DFS with explicit stack
  • Know pre-order (process before recursion) vs post-order (process after)
  • Know DFS finish-order: push to stack AFTER all neighbors processed
Cycle Detection
  • Undirected: DFS with parent tracking — cycle when visited neighbor ≠ parent
  • Directed: 3-color DFS (0=white, 1=gray, 2=black) — cycle when gray neighbor found
Topological Sort
  • Kahn's BFS: in-degree array → queue of zero-in-degree → process and reduce
  • Know: if processed count < n → cycle exists
  • DFS: post-order push to stack → pop for topological order
  • Apply to Course Schedule (cycle detection) and Course Schedule II (order)
Connected Components
  • Count components: DFS/BFS from each unvisited node, increment counter
  • Label components: assign component IDs
  • Flood fill on grid: in-place marking (grid[r][c] = '0')
SCC (Kosaraju's)
  • Two-pass: finish-order DFS on original → transpose graph → DFS in reverse finish order
  • Know that each DFS tree in the second pass is exactly one SCC
Bipartite Check
  • BFS 2-coloring: assign colors alternately, return false if same-color neighbors found
  • A graph with odd cycles is NOT bipartite
Advanced Patterns
  • Apply multi-source BFS for min-distance-from-any-source problems
  • Apply 0-1 BFS with deque (push_front for 0-weight, push_back for 1-weight)
  • Know Hierholzer's algorithm for Eulerian path (follow edges without repetition)
Notes▸