Skip to content
DSA projects 9 min read

Project: Pathfinding Visualizer (BFS & Dijkstra on a Grid)

In this project you’ll build the engine behind a pathfinding visualizer — the part that, given a grid with walls, finds the shortest route from a start cell to a target. A grid is just a graph in disguise: each open cell is a node, and adjacent cells are connected by edges. So the right tools are graph search: Breadth-First Search (BFS) when every step costs the same, and Dijkstra’s algorithm when steps have different costs. Read Breadth-First Search and Dijkstra’s Shortest Path for the underlying theory.

What you’ll build

Given a 2D grid where 0 is open and 1 is a wall, plus a start and target cell, return the length (or cost) of the shortest path, or -1 if the target is unreachable. We move in the four cardinal directions. This core function is what a UI would call on every frame to draw the path.

Part 1 — unweighted grids with BFS

When every move costs exactly one step, BFS is optimal: it explores the grid in expanding rings, so the first time it reaches the target, it has used the fewest steps possible. The key technique is processing the queue one level at a time — count how many cells are in the queue, drain exactly that many, then increment the step counter. Mark cells visited as you enqueue them (not when you dequeue) so the same cell never enters the queue twice.

#include <vector>
#include <queue>
#include <utility>

// grid: 0 = open, 1 = wall. Returns steps from start to target, or -1.
int shortestPath(const std::vector<std::vector<int>>& grid,
                 std::pair<int,int> start, std::pair<int,int> target) {
    int rows = grid.size(), cols = grid[0].size();
    std::vector<std::vector<bool>> visited(rows, std::vector<bool>(cols, false));
    std::queue<std::pair<int,int>> q;
    q.push(start);
    visited[start.first][start.second] = true;
    int dr[] = {-1, 1, 0, 0}, dc[] = {0, 0, -1, 1};
    int steps = 0;
    while (!q.empty()) {
        int sz = q.size();
        for (int i = 0; i < sz; i++) {
            auto [r, c] = q.front(); q.pop();
            if (r == target.first && c == target.second) return steps;
            for (int d = 0; d < 4; d++) {
                int nr = r + dr[d], nc = c + dc[d];
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols
                    && !visited[nr][nc] && grid[nr][nc] == 0) {
                    visited[nr][nc] = true;
                    q.push({nr, nc});
                }
            }
        }
        steps++;
    }
    return -1;
}

Gotcha: BFS is only optimal when every edge has the same weight. The moment some cells are “slow” (mud, water), BFS can return a path that’s short in steps but expensive in cost. That’s where Dijkstra comes in.

Part 2 — weighted grids with Dijkstra

Now let cost[r][c] be the cost to step onto that cell. Dijkstra generalizes BFS by always expanding the cheapest-known cell next, using a min-priority queue keyed on distance. Keep a dist grid of best-known costs; when you pop a cell whose stored distance is worse than the best you’ve recorded, skip it (a stale duplicate). The first time you pop the target, its distance is final.

#include <vector>
#include <queue>
#include <tuple>
#include <climits>

// cost[r][c] = cost to step onto that cell. Returns min cost, or -1.
int dijkstraGrid(const std::vector<std::vector<int>>& cost,
                 std::pair<int,int> start, std::pair<int,int> target) {
    int rows = cost.size(), cols = cost[0].size();
    std::vector<std::vector<int>> dist(rows, std::vector<int>(cols, INT_MAX));
    using State = std::tuple<int,int,int>; // (distance, row, col)
    std::priority_queue<State, std::vector<State>, std::greater<State>> pq;
    dist[start.first][start.second] = 0;
    pq.push({0, start.first, start.second});
    int dr[] = {-1, 1, 0, 0}, dc[] = {0, 0, -1, 1};
    while (!pq.empty()) {
        auto [d, r, c] = pq.top(); pq.pop();
        if (r == target.first && c == target.second) return d;
        if (d > dist[r][c]) continue; // stale entry
        for (int k = 0; k < 4; k++) {
            int nr = r + dr[k], nc = c + dc[k];
            if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
            int nd = d + cost[nr][nc];
            if (nd < dist[nr][nc]) {
                dist[nr][nc] = nd;
                pq.push({nd, nr, nc});
            }
        }
    }
    return -1;
}

Complexity

For a grid with V = rows × cols cells and at most 4V edges:

AlgorithmUse whenTimeSpace
BFSall steps cost the sameO(V)O(V)
Dijkstrasteps have different costsO(V log V)O(V)

Going deeper: To draw the actual path (not just its length), store a parent for each cell as you discover it, then walk parents back from the target. For faster searches toward a known target, add a heuristic and you’ve turned Dijkstra into A*. To visualize the search, record the order cells are visited and animate it frame by frame.

Next project: a mini database index.

Last updated June 25, 2026
Was this helpful?