Skip to content

Graphs: BFS, DFS, Connected Components and Topological Sort

Store a graph the right way, then use breadth-first and depth-first search for fewest-step routes, grids, components, two-colorings, build orders and cycles, all in O(V + E).

Graphs hide in problem statements

Few problems say "graph". They say stops and tram links, cells of a map, guests who refuse to sit together, modules that must be built before others. Each of those is a set of things (vertices) and pairs of things that are related (edges). Once you see the graph, a small set of algorithms answers a surprising number of questions: the fewest steps from here to there, which things are connected, whether the things can be split into two compatible groups, and in what order to process them when some must come before others.

All of these are traversals: a systematic walk that visits every reachable vertex once and looks at every edge a constant number of times. That gives the bound that makes them so useful, O(V + E) for V vertices and E edges. With V and E up to a few hundred thousand, the complexity table says that is comfortably fast, and it is the best you can hope for, since reading the input already costs that much.

The usual failures are not about the idea. They are about details: an undirected edge stored in one direction only, a vertex marked visited at the wrong moment, a graph assumed to be connected when it is not, a recursion that is too deep for the stack. This lesson builds each traversal, shows those failures running, and stress tests the code against slow but obviously correct versions, as set up in the previous lesson.

Three ways to store a graph

The input usually gives n and m, then m lines with two vertex numbers each: an edge list. What you build from it decides what the algorithm can do quickly.

RepresentationMemoryNeighbors of uIs (u, v) an edge?Good for
Adjacency listO(V + E)O(deg u)O(deg u)almost every traversal
Edge listO(E)O(E)O(E)sorting edges (Kruskal, lesson 5), Bellman-Ford
Adjacency matrixO(V^2)O(V)O(1)dense graphs, V up to a few thousand, Floyd-Warshall

The adjacency list in C++ is a std::vector<std::vector<int>> where adj[u] holds the neighbors of u. A matrix of V = 10^5 vertices would need 10^10 entries, which is why it is out of the question for sparse graphs, while the list needs only V + 2E integers.

Two input conventions cause more wrong answers than the algorithms do:

  • Numbering. Most statements number vertices from 1. Either subtract 1 as you read (as in this lesson) and add it back when you print, or allocate n + 1 entries and ignore index 0. Pick one and do it everywhere.
  • Direction. An undirected edge ("a two-way link", "they are rivals") goes into both lists: adj[a].push_back(b) and adj[b].push_back(a). A directed edge ("a must come before b") goes into one. The pitfall section shows what happens when you mix them up.

BFS: fewest steps, one layer at a time

Tram routes. A town's tram network has n stops and m two-way links between them (n, m <= 2*10^5). Find the fewest links needed to ride from stop s to stop t, and one such route, or print -1 if t cannot be reached.

Every link counts the same, so the question is about the number of edges on a path. Breadth-first search (BFS) answers it. Start with s at distance 0. Its neighbors are at distance 1. Their unvisited neighbors are at distance 2, and so on, layer by layer. A first-in first-out queue processes the vertices in exactly that order: all of layer k leaves the queue before any of layer k + 1. The first time a vertex is reached, it is reached by a shortest route, because every vertex of a smaller layer was processed earlier.

To print the route, remember for each vertex the one it was reached from (parent). Following parents back from t gives the route in reverse.

The input is n m s t, then the links:

7 8 1 7
1 2
1 3
2 4
3 4
4 5
5 7
3 6
6 7
main.cppc++23
#include <algorithm>
#include <iostream>
#include <print>
#include <queue>
#include <sstream>
#include <vector>

void solve(std::istream& in, std::ostream& out) {
    int n, m, s, t;
    in >> n >> m >> s >> t;
    --s, --t;  // the input numbers stops from 1; we store them from 0
    std::vector<std::vector<int>> adj(n);
    for (int i = 0; i < m; ++i) {
        int a, b;
        in >> a >> b;
        --a, --b;
        adj[a].push_back(b);  // a two-way link goes into both lists
        adj[b].push_back(a);
    }

    std::vector<int> dist(n, -1), parent(n, -1);
    std::queue<int> q;
    dist[s] = 0;
    q.push(s);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : adj[u]) {
            if (dist[v] != -1) continue;  // already reached, by a route no longer
            dist[v] = dist[u] + 1;
            parent[v] = u;
            q.push(v);
        }
    }

    if (dist[t] == -1) { std::println(out, "-1"); return; }
    std::vector<int> route;
    for (int v = t; v != -1; v = parent[v]) route.push_back(v + 1);
    std::ranges::reverse(route);
    std::print(out, "{} links:", dist[t]);
    for (int stop : route) std::print(out, " {}", stop);
    std::println(out);
}

int main() {
    std::istringstream sample("7 8 1 7\n1 2\n1 3\n2 4\n3 4\n4 5\n5 7\n3 6\n6 7\n");
    solve(sample, std::cout);  // on a judge: solve(std::cin, std::cout)
}
Output

Press Run to compile and run this example.

3 links: 1 3 6 7

Two routes from stop 1 lead towards 7: 1 2 4 5 7 with four links and 1 3 6 7 with three. BFS reaches stop 7 first through stop 6, which is in layer 2, so it reports three links. Every vertex enters the queue at most once and every adjacency list is scanned once, so this is O(V + E).

Grids and many sources at once

A map of cells is a graph too, with an edge between each pair of adjacent free cells. You do not need to build the adjacency list: the neighbors of cell (r, c) come from a direction table, a small array of row and column offsets. Looping over the table replaces four copies of almost the same code, and switching to 8 directions changes only the table.

Charging stations. A warehouse floor is a grid of free cells ., shelves # and charging stations C. A robot moves up, down, left or right. For each free cell, how many moves is the nearest charging station away?

Running BFS once from every station would cost O(stations * cells). Instead, put all the stations into the queue at distance 0 before the search starts. This multi-source BFS behaves as if there were one extra vertex connected to every station: each cell is reached first from the closest station, in one O(rows * cols) pass.

5 9
C...#....
.##.#.##.
....#.#C.
.#..#.###
.#....#.#
main.cppc++23
#include <array>
#include <iostream>
#include <print>
#include <queue>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

constexpr std::array<std::pair<int, int>, 4> kSteps{{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}};

void solve(std::istream& in, std::ostream& out) {
    int rows, cols;
    in >> rows >> cols;
    std::vector<std::string> grid(rows);
    for (auto& row : grid) in >> row;

    std::vector<std::vector<int>> dist(rows, std::vector<int>(cols, -1));
    std::queue<std::pair<int, int>> q;
    for (int r = 0; r < rows; ++r)
        for (int c = 0; c < cols; ++c)
            if (grid[r][c] == 'C') { dist[r][c] = 0; q.push({r, c}); }  // all sources

    while (!q.empty()) {
        auto [r, c] = q.front();
        q.pop();
        for (auto [dr, dc] : kSteps) {
            int nr = r + dr, nc = c + dc;
            if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;  // off the map
            if (grid[nr][nc] == '#' || dist[nr][nc] != -1) continue;
            dist[nr][nc] = dist[r][c] + 1;
            q.push({nr, nc});
        }
    }

    for (int r = 0; r < rows; ++r) {
        for (int c = 0; c < cols; ++c) {
            if (grid[r][c] == '#') std::print(out, "  #");
            else if (dist[r][c] == -1) std::print(out, "  -");
            else std::print(out, "{:>3}", dist[r][c]);
        }
        std::println(out);
    }
}

int main() {
    std::istringstream sample("5 9\n"
                              "C...#....\n"
                              ".##.#.##.\n"
                              "....#.#C.\n"
                              ".#..#.###\n"
                              ".#....#.#\n");
    solve(sample, std::cout);
}
Output

Press Run to compile and run this example.

  0  1  2  3  #  6  5  4  3
  1  #  #  4  #  7  #  #  2
  2  3  4  5  #  8  #  0  1
  3  #  5  6  #  9  #  #  #
  4  #  6  7  8  9  #  -  #

The left part of the map is served by the station in the top-left corner, the right part by the one in row 3. The top-right corner is 3 moves from the station in row 3; from the top-left station it could be reached only by a long detour through the bottom row, and the multi-source queue never even tries that detour. The cell marked - in the bottom row is boxed in by shelves, so no station reaches it. Always decide what to print for unreachable cells; the statement usually says.

DFS: going deep, and the times it leaves behind

Depth-first search (DFS) explores differently: from a vertex, follow one unvisited edge as far as possible, and only when stuck, back up and try the next edge. Recursion expresses this directly. DFS does not find shortest paths, but the order in which it starts and finishes vertices carries information that BFS does not have.

Record two timestamps per vertex: in when the search first enters it and out when it has finished everything reachable from it. If v is discovered while u is still open, v is a descendant of u, and its whole interval [in, out] lies inside u's. Topological sort and cycle detection below are built on these intervals.

The same search can also be written with an explicit stack instead of recursion. That matters because recursion depth is limited by the size of the call stack, which is fixed by the operating system and the judge. On a path graph 0 -> 1 -> ... -> n-1, recursive DFS nests n calls deep. With n = 10^5 or 10^6 and a few dozen bytes per call frame, that can exceed the stack (the Microsoft linker's default stack reservation, for example, is 1 MB), and the program crashes with no useful message, usually only on the largest test. The iterative version keeps a std::stack of pairs (vertex, index of the next neighbor to try) on the heap, so it reproduces the recursion step by step, with the same timestamps.

main.cppc++23
#include <algorithm>
#include <print>
#include <stack>
#include <utility>
#include <vector>

using Graph = std::vector<std::vector<int>>;

struct Times { std::vector<int> in, out; };

Times recursiveDfs(const Graph& g) {
    int n = int(g.size()), timer = 0;
    Times t{std::vector<int>(n, -1), std::vector<int>(n, -1)};
    auto visit = [&](this auto&& self, int u) -> void {
        t.in[u] = timer++;
        for (int v : g[u])
            if (t.in[v] == -1) self(v);
        t.out[u] = timer++;
    };
    for (int u = 0; u < n; ++u)
        if (t.in[u] == -1) visit(u);  // the graph may have several parts
    return t;
}

// Same order of visits, but the call stack is an explicit std::stack of
// (vertex, index of the next neighbor to look at).
Times iterativeDfs(const Graph& g, std::size_t& deepest) {
    int n = int(g.size()), timer = 0;
    Times t{std::vector<int>(n, -1), std::vector<int>(n, -1)};
    for (int root = 0; root < n; ++root) {
        if (t.in[root] != -1) continue;
        std::stack<std::pair<int, std::size_t>> st;
        st.push({root, 0});
        t.in[root] = timer++;
        while (!st.empty()) {
            deepest = std::max(deepest, st.size());
            auto& [u, next] = st.top();
            if (next < g[u].size()) {
                int v = g[u][next++];
                if (t.in[v] == -1) { t.in[v] = timer++; st.push({v, 0}); }
            } else {
                t.out[u] = timer++;
                st.pop();
            }
        }
    }
    return t;
}

int main() {
    Graph g = {{1, 2}, {3}, {3}, {}, {5}, {}};  // directed: 0->1, 0->2, 1->3, ...
    std::size_t deepest = 0;
    Times a = recursiveDfs(g), b = iterativeDfs(g, deepest);
    for (std::size_t v = 0; v < g.size(); ++v)
        std::println("vertex {}: in {:>2}, out {:>2}", v, a.in[v], a.out[v]);
    std::println("iterative version gives the same times: {}",
                 a.in == b.in && a.out == b.out);

    const int n = 100'000;
    Graph path(n);  // 0 -> 1 -> 2 -> ... -> n-1
    for (int v = 0; v + 1 < n; ++v) path[v].push_back(v + 1);
    deepest = 0;
    iterativeDfs(path, deepest);
    std::println("path of {} vertices: explicit stack reached depth {}", n, deepest);
}
Output

Press Run to compile and run this example.

vertex 0: in  0, out  7
vertex 1: in  1, out  4
vertex 2: in  5, out  6
vertex 3: in  2, out  3
vertex 4: in  8, out 11
vertex 5: in  9, out 10
iterative version gives the same times: true
path of 100000 vertices: explicit stack reached depth 100000

Vertices 4 and 5 are not reachable from 0, so the outer loop starts a second search from 4. Forgetting that loop is a classic bug: the program works whenever the test graph is connected. The last line shows the explicit stack growing to 10^5 entries on the path graph, which is harmless on the heap. The recursive version would need the same number of nested calls.

Components and two-coloring

A connected component of an undirected graph is a maximal set of vertices that can all reach each other. To find them all, loop over the vertices and start a new BFS (or DFS) from each vertex that is not yet visited; each start is a new component. The total work is still O(V + E), because each vertex is visited once across all the searches.

A graph is bipartite if its vertices can be split into two groups with every edge going between the groups. BFS decides this while it traverses. Give the start vertex side A, every neighbor the opposite side of the vertex it was reached from, and check every edge: an edge between two vertices on the same side means no split exists. Such a conflict happens exactly when a component contains a cycle of odd length.

Two tables. At a dinner, n guests are seated at two tables, and m pairs of guests are rivals who must not sit at the same table. For each group of guests linked by rivalries, print a seating or say why it cannot work.

8 7
1 2
2 3
3 4
4 1
5 6
6 7
7 5
main.cppc++23
#include <iostream>
#include <print>
#include <queue>
#include <sstream>
#include <string>
#include <vector>

void solve(std::istream& in, std::ostream& out) {
    int n, m;
    in >> n >> m;
    std::vector<std::vector<int>> adj(n);
    for (int i = 0; i < m; ++i) {
        int a, b;
        in >> a >> b;
        adj[a - 1].push_back(b - 1);
        adj[b - 1].push_back(a - 1);
    }

    std::vector<int> table(n, -1);  // -1: not seated yet; otherwise 0 or 1
    int components = 0;
    for (int start = 0; start < n; ++start) {
        if (table[start] != -1) continue;  // already in an earlier component
        ++components;
        std::vector<int> members;
        int clashA = -1, clashB = -1;
        std::queue<int> q;
        table[start] = 0;
        q.push(start);
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            members.push_back(u);
            for (int v : adj[u]) {
                if (table[v] == -1) { table[v] = 1 - table[u]; q.push(v); }
                else if (table[v] == table[u] && clashA == -1) clashA = u, clashB = v;
            }
        }
        std::print(out, "group {}: guests", components);
        for (int g : members) std::print(out, " {}", g + 1);
        if (clashA != -1) {
            std::println(out, ": impossible, rivals {} and {} share a table",
                         clashA + 1, clashB + 1);
        } else {
            std::string plan;
            for (int g : members) plan += table[g] == 0 ? 'A' : 'B';
            std::println(out, ": tables {}", plan);
        }
    }
}

int main() {
    std::istringstream sample("8 7\n1 2\n2 3\n3 4\n4 1\n5 6\n6 7\n7 5\n");
    solve(sample, std::cout);
}
Output

Press Run to compile and run this example.

group 1: guests 1 2 4 3: tables ABBA
group 2: guests 5 6 7: impossible, rivals 6 and 7 share a table
group 3: guests 8: tables A

Guests 1 to 4 form a cycle of four rivalries, which alternates fine. Guests 5, 6 and 7 form a triangle: whoever sits at A, the other two must both sit at B, and they are rivals. Guest 8 has no rivals and forms a group alone, which the loop over all start vertices handles without a special case.

Topological order, and finding the cycle when there is none

In a directed graph where an edge a -> b means "a must come before b", a topological order lists all vertices so that every edge points forward. It exists exactly when the graph has no directed cycle, that is, when it is a directed acyclic graph (DAG).

Build order. A project has n modules and m rules "module a must be built before module b". Print an order that respects all rules, or, if none exists, a cycle of rules that makes it impossible.

Kahn's algorithm (A. B. Kahn, 1962) works like a build tool. Count for each module how many unbuilt modules it still waits for (its in-degree). Every module with in-degree 0 can be built now; build one, and decrease the in-degree of everything that waited for it. If modules remain but none has in-degree 0, every remaining module waits for another remaining one, which means there is a cycle. So Kahn's algorithm both sorts and detects cycles: the order it produces is shorter than n exactly when the graph has a cycle.

The DFS-based method uses finishing times instead: in a DAG, listing vertices in decreasing out time gives a topological order, because every edge u -> v has v finished before u. DFS is also the natural way to find an actual cycle. Color vertices white (not visited), gray (on the current path of the search) and black (finished). An edge to a gray vertex leads back to a vertex on the current path, so it closes a cycle, and the parent links spell it out.

The first sample is acyclic; the second adds the rule 5 2, which closes the loop 2 -> 4 -> 5 -> 2.

6 6          6 7
1 2          1 2
1 3          1 3
2 4          2 4
3 4          3 4
4 5          4 5
6 5          6 5
             5 2
main.cppc++23
#include <algorithm>
#include <iostream>
#include <print>
#include <queue>
#include <sstream>
#include <vector>

using Graph = std::vector<std::vector<int>>;

// Kahn: repeatedly take a module that nothing unbuilt still needs.
std::vector<int> kahn(const Graph& g) {
    int n = int(g.size());
    std::vector<int> indegree(n, 0), order;
    for (const auto& outs : g)
        for (int v : outs) ++indegree[v];
    std::queue<int> ready;
    for (int v = 0; v < n; ++v)
        if (indegree[v] == 0) ready.push(v);
    while (!ready.empty()) {
        int u = ready.front();
        ready.pop();
        order.push_back(u);
        for (int v : g[u])
            if (--indegree[v] == 0) ready.push(v);
    }
    return order;  // shorter than n exactly when there is a cycle
}

// DFS with three colors; returns a cycle (first vertex repeated at the end) or {}.
std::vector<int> findCycle(const Graph& g) {
    enum Color { White, Gray, Black };  // unvisited, on the current path, finished
    int n = int(g.size());
    std::vector<Color> color(n, White);
    std::vector<int> parent(n, -1), cycle;
    auto visit = [&](this auto&& self, int u) -> bool {
        color[u] = Gray;
        for (int v : g[u]) {
            if (color[v] == Gray) {  // back edge u -> v closes a cycle
                for (int w = u; w != v; w = parent[w]) cycle.push_back(w);
                cycle.push_back(v);
                std::ranges::reverse(cycle);
                cycle.push_back(v);
                return true;
            }
            if (color[v] == White) {
                parent[v] = u;
                if (self(v)) return true;
            }
        }
        color[u] = Black;
        return false;
    };
    for (int u = 0; u < n; ++u)
        if (color[u] == White && visit(u)) break;
    return cycle;
}

void solve(std::istream& in, std::ostream& out) {
    int n, m;
    in >> n >> m;
    Graph g(n);
    for (int i = 0; i < m; ++i) {
        int before, after;
        in >> before >> after;
        g[before - 1].push_back(after - 1);  // directed: one list only
    }
    std::vector<int> order = kahn(g);
    if (int(order.size()) == n) {
        std::print(out, "build order:");
        for (int v : order) std::print(out, " {}", v + 1);
    } else {
        std::print(out, "no order; cycle:");
        for (int v : findCycle(g)) std::print(out, " {}", v + 1);
    }
    std::println(out);
}

int main() {
    std::istringstream ok("6 6\n1 2\n1 3\n2 4\n3 4\n4 5\n6 5\n");
    std::istringstream cyclic("6 7\n1 2\n1 3\n2 4\n3 4\n4 5\n6 5\n5 2\n");
    solve(ok, std::cout);
    solve(cyclic, std::cout);
}
Output

Press Run to compile and run this example.

build order: 1 6 2 3 4 5
no order; cycle: 2 4 5 2

Kahn's queue starts with modules 1 and 6, both of which need nothing, and the rest follows. A topological order is usually not unique: 6 1 3 2 4 5 would be just as valid. If a problem asks for the smallest order in dictionary order, replace the queue with a min-priority queue (std::priority_queue<int, std::vector<int>, std::greater<>>), which costs an extra log V per vertex.