LearnFrenzy
Sign in Take a free test

DSA and Competitive Programming · Lesson 29 of 33

Graphs in C++: BFS, DFS and Dijkstra

6 min read Updated Nov 7, 2026 Saurabh Samir

Graphs model networks of things: cities and roads, users and friendships, web pages and links, tasks and dependencies. With the STL, a graph is just a vector of vectors, and the three core algorithms – BFS, DFS and Dijkstra – are each about twenty lines. They appear in nearly every DSA round.

In this lesson

  • Adjacency lists
  • BFS: shortest paths in unweighted graphs
  • DFS: connected components and grids
  • Dijkstra: shortest paths with weights

Adjacency list

#include <iostream>
#include <vector>

int main() {
    int n = 5;
    std::vector<std::vector<int>> adj(n);
    auto add_edge = [&](int u, int v) { adj[u].push_back(v); adj[v].push_back(u); };   // undirected
    add_edge(0, 1); add_edge(0, 2); add_edge(1, 3); add_edge(2, 3); add_edge(3, 4);

    for (int u = 0; u < n; u++) {
        std::cout << u << ':';
        for (int v : adj[u]) std::cout << ' ' << v;
        std::cout << '\n';
    }
    return 0;
}

Output

0: 1 2
1: 0 3
2: 0 3
3: 1 2 4
4: 3

An adjacency list uses O(V + E) memory and lets you visit a node's neighbours directly. An adjacency matrix (vector<vector<bool>> g(n, vector<bool>(n))) answers "is there an edge u–v?" in O(1) but needs O(V²) memory – only practical for small, dense graphs.

BFS: fewest edges

#include <iostream>
#include <queue>
#include <vector>

int main() {
    int n = 6;
    std::vector<std::vector<int>> adj(n);
    for (auto [u, v] : std::vector<std::pair<int, int>>{{0, 1}, {0, 2}, {1, 3}, {2, 3}, {3, 4}, {4, 5}}) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    std::vector<int> dist(n, -1), parent(n, -1);
    std::queue<int> q;
    dist[0] = 0;
    q.push(0);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u]) {
            if (dist[v] == -1) {                 // not visited yet
                dist[v] = dist[u] + 1;
                parent[v] = u;
                q.push(v);
            }
        }
    }
    for (int v = 0; v < n; v++) std::cout << "dist to " << v << " = " << dist[v] << '\n';

    std::vector<int> path;                       // rebuild the path 0 → 5 from parents
    for (int v = 5; v != -1; v = parent[v]) path.insert(path.begin(), v);
    std::cout << "path:";
    for (int v : path) std::cout << ' ' << v;
    std::cout << '\n';
    return 0;
}

Output

dist to 0 = 0
dist to 1 = 1
dist to 2 = 1
dist to 3 = 2
dist to 4 = 3
dist to 5 = 4
path: 0 1 3 4 5

BFS explores in waves

  1. Level 0the source
  2. Level 1all its neighbours
  3. Level 2their unvisited neighbours…
  4. Resultthe first time a node is reached is via a shortest path – O(V + E)

DFS: connected components and grids

#include <iostream>
#include <string>
#include <vector>

std::vector<std::string> grid{
    "11000",
    "11010",
    "00011",
    "10000",
};
int rows = 4, cols = 5;

void sink(int r, int c) {                                  // flood fill from (r, c)
    if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] != '1') return;
    grid[r][c] = '0';                                      // mark visited
    sink(r + 1, c); sink(r - 1, c); sink(r, c + 1); sink(r, c - 1);
}

int main() {
    int islands = 0;
    for (int r = 0; r < rows; r++)
        for (int c = 0; c < cols; c++)
            if (grid[r][c] == '1') { islands++; sink(r, c); }
    std::cout << "islands: " << islands << '\n';
    return 0;
}

Output

islands: 3

DFS goes as deep as possible before backtracking – here with recursion. It's used for connected components, cycle detection, topological sort and flood fill. For very deep graphs (10⁵+ nodes in a line), recursion can overflow the stack; use an explicit std::stack instead.

Dijkstra: weighted shortest paths

#include <functional>
#include <iostream>
#include <limits>
#include <queue>
#include <string>
#include <utility>
#include <vector>

int main() {
    std::vector<std::string> city{"Pune", "Mumbai", "Nashik", "Lonavala", "Satara"};
    int n = 5;
    std::vector<std::vector<std::pair<int, int>>> adj(n);      // (neighbour, km)
    auto road = [&](int u, int v, int w) { adj[u].push_back({v, w}); adj[v].push_back({u, w}); };
    road(0, 3, 65); road(3, 1, 83); road(0, 2, 210); road(1, 2, 167); road(0, 4, 112);

    const long long INF = std::numeric_limits<long long>::max();
    std::vector<long long> dist(n, INF);
    using State = std::pair<long long, int>;                    // (distance so far, node)
    std::priority_queue<State, std::vector<State>, std::greater<State>> pq;   // min heap
    dist[0] = 0;
    pq.push({0, 0});
    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (d > dist[u]) continue;                              // stale entry: skip
        for (auto [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
    for (int v = 1; v < n; v++) std::cout << "Pune -> " << city[v] << ": " << dist[v] << " km\n";
    return 0;
}

Output

Pune -> Mumbai: 148 km
Pune -> Nashik: 210 km
Pune -> Lonavala: 65 km
Pune -> Satara: 112 km
AlgorithmGraphFindsComplexity
BFSunweightedfewest edgesO(V + E)
DFSanycomponents, cycles, topological orderO(V + E)
Dijkstranon-negative weightsshortest weighted pathsO((V + E) log V)
Bellman–Fordnegative weights allowedshortest paths, negative cyclesO(V · E)

Dijkstra pitfalls

  • It's wrong with negative edge weights – use Bellman–Ford.
  • Forgetting the "stale entry" check makes it much slower on dense graphs.
  • Use long long distances: sums of many weights overflow int.

Practice

Try it yourself

  1. Count the connected components of a graph with 6 nodes and edges 0–1, 1–2, 3–4 using DFS with an explicit stack.

    Show solution
    #include <iostream>
    #include <stack>
    #include <vector>
    
    int main() {
        int n = 6;
        std::vector<std::vector<int>> adj(n);
        for (auto [u, v] : std::vector<std::pair<int, int>>{{0, 1}, {1, 2}, {3, 4}}) {
            adj[u].push_back(v);
            adj[v].push_back(u);
        }
        std::vector<bool> seen(n, false);
        int components = 0;
        for (int s = 0; s < n; s++) {
            if (seen[s]) continue;
            components++;
            std::stack<int> st;
            st.push(s);
            seen[s] = true;
            while (!st.empty()) {
                int u = st.top(); st.pop();
                for (int v : adj[u]) if (!seen[v]) { seen[v] = true; st.push(v); }
            }
        }
        std::cout << components << " components\n";
        return 0;
    }

    Output

    3 components

Quick check

Which algorithm finds the fewest-edges path?

BFS.

Why does Dijkstra use a min heap?

To always expand the closest unfinished node next.

Can Dijkstra handle negative weights?

No – use Bellman–Ford.

Summary

  • Store graphs as adjacency lists: vector<vector<int>> (or pairs for weights).
  • BFS (queue) gives unweighted shortest paths; DFS (recursion/stack) explores components.
  • Dijkstra uses a min-heap priority_queue for non-negative weighted shortest paths.

Interview practice: graphs, BFS, DFS and shortest path questions.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular