Graph Traversal

Graph traversal algorithms are fundamental techniques for exploring graphs systematically. DFS and BFS form the foundation for many advanced graph algorithms.

Depth-First Search (DFS)

DFS explores as far as possible along each branch before backtracking. It uses a stack (implicit with recursion or explicit).

Basic DFS Implementation

vector<vector<int>> adj;
vector<bool> visited;

void dfs(int v) {
    visited[v] = true;
    cout << v << " "; // Process vertex
    
    for (int neighbor : adj[v]) {
        if (!visited[neighbor]) {
            dfs(neighbor);
        }
    }
}

// Usage
int n = 5;
adj.resize(n + 1);
visited.resize(n + 1, false);

// Start DFS from vertex 1
dfs(1);

Iterative DFS

void dfs_iterative(int start) {
    stack<int> st;
    vector<bool> visited(n + 1, false);
    
    st.push(start);
    
    while (!st.empty()) {
        int v = st.top();
        st.pop();
        
        if (visited[v]) continue;
        
        visited[v] = true;
        cout << v << " ";
        
        // Add neighbors in reverse order for same traversal as recursive
        for (int i = adj[v].size() - 1; i >= 0; i--) {
            int neighbor = adj[v][i];
            if (!visited[neighbor]) {
                st.push(neighbor);
            }
        }
    }
}

DFS Properties:

  • Time Complexity: O(V + E)
  • Space Complexity: O(V) for recursion stack
  • Use Cases: Cycle detection, topological sort, strongly connected components

Breadth-First Search (BFS)

BFS explores all vertices at the current depth before moving to vertices at the next depth level. It uses a queue.

Basic BFS Implementation

void bfs(int start) {
    queue<int> q;
    vector<bool> visited(n + 1, false);
    
    q.push(start);
    visited[start] = true;
    
    while (!q.empty()) {
        int v = q.front();
        q.pop();
        
        cout << v << " "; // Process vertex
        
        for (int neighbor : adj[v]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                q.push(neighbor);
            }
        }
    }
}

BFS with Distance Calculation

vector<int> bfs_distance(int start) {
    vector<int> dist(n + 1, -1); // -1 means not visited
    queue<int> q;
    
    q.push(start);
    dist[start] = 0;
    
    while (!q.empty()) {
        int v = q.front();
        q.pop();
        
        for (int neighbor : adj[v]) {
            if (dist[neighbor] == -1) { // Not visited
                dist[neighbor] = dist[v] + 1;
                q.push(neighbor);
            }
        }
    }
    
    return dist; // Returns shortest distance to all vertices
}

BFS for Shortest Path:

In unweighted graphs, BFS guarantees the shortest path from source to all other vertices.

Connected Components

Find all connected components in an undirected graph using DFS or BFS.

vector<vector<int>> components;
vector<bool> visited;

void dfs_component(int v, vector<int>& component) {
    visited[v] = true;
    component.push_back(v);
    
    for (int neighbor : adj[v]) {
        if (!visited[neighbor]) {
            dfs_component(neighbor, component);
        }
    }
}

void find_components() {
    visited.assign(n + 1, false);
    components.clear();
    
    for (int v = 1; v <= n; v++) {
        if (!visited[v]) {
            vector<int> component;
            dfs_component(v, component);
            components.push_back(component);
        }
    }
    
    cout << "Number of components: " << components.size() << endl;
}

Cycle Detection

Cycle Detection in Undirected Graph

bool has_cycle_undirected() {
    vector<bool> visited(n + 1, false);
    
    function<bool(int, int)> dfs = [&](int v, int parent) -> bool {
        visited[v] = true;
        
        for (int neighbor : adj[v]) {
            if (neighbor == parent) continue; // Skip parent edge
            
            if (visited[neighbor] || dfs(neighbor, v)) {
                return true; // Cycle found
            }
        }
        return false;
    };
    
    for (int v = 1; v <= n; v++) {
        if (!visited[v] && dfs(v, -1)) {
            return true;
        }
    }
    return false;
}

Cycle Detection in Directed Graph

bool has_cycle_directed() {
    vector<int> color(n + 1, 0); // 0 = white, 1 = gray, 2 = black
    
    function<bool(int)> dfs = [&](int v) -> bool {
        color[v] = 1; // Mark as gray (being processed)
        
        for (int neighbor : adj[v]) {
            if (color[neighbor] == 1) { // Back edge to gray vertex
                return true; // Cycle found
            }
            if (color[neighbor] == 0 && dfs(neighbor)) {
                return true;
            }
        }
        
        color[v] = 2; // Mark as black (fully processed)
        return false;
    };
    
    for (int v = 1; v <= n; v++) {
        if (color[v] == 0 && dfs(v)) {
            return true;
        }
    }
    return false;
}

Topological Sorting

Linear ordering of vertices in a directed acyclic graph (DAG) such that for every directed edge (u, v), u comes before v in the ordering.

DFS-based Topological Sort

vector<int> topological_sort() {
    vector<bool> visited(n + 1, false);
    stack<int> result;
    
    function<void(int)> dfs = [&](int v) {
        visited[v] = true;
        
        for (int neighbor : adj[v]) {
            if (!visited[neighbor]) {
                dfs(neighbor);
            }
        }
        
        result.push(v); // Add to result after processing all descendants
    };
    
    for (int v = 1; v <= n; v++) {
        if (!visited[v]) {
            dfs(v);
        }
    }
    
    vector<int> topo_order;
    while (!result.empty()) {
        topo_order.push_back(result.top());
        result.pop();
    }
    
    return topo_order;
}

Kahn's Algorithm (BFS-based)

vector<int> kahn_topological_sort() {
    vector<int> in_degree(n + 1, 0);
    
    // Calculate in-degrees
    for (int v = 1; v <= n; v++) {
        for (int neighbor : adj[v]) {
            in_degree[neighbor]++;
        }
    }
    
    queue<int> q;
    for (int v = 1; v <= n; v++) {
        if (in_degree[v] == 0) {
            q.push(v);
        }
    }
    
    vector<int> result;
    while (!q.empty()) {
        int v = q.front();
        q.pop();
        result.push_back(v);
        
        for (int neighbor : adj[v]) {
            in_degree[neighbor]--;
            if (in_degree[neighbor] == 0) {
                q.push(neighbor);
            }
        }
    }
    
    // Check if graph has cycle
    if (result.size() != n) {
        return {}; // Graph has cycle, no topological ordering
    }
    
    return result;
}

Bipartite Graph Detection

A bipartite graph can be colored with two colors such that no two adjacent vertices have the same color.

bool is_bipartite() {
    vector<int> color(n + 1, -1); // -1 = uncolored, 0 = color1, 1 = color2
    
    function<bool(int, int)> dfs = [&](int v, int c) -> bool {
        color[v] = c;
        
        for (int neighbor : adj[v]) {
            if (color[neighbor] == -1) {
                if (!dfs(neighbor, 1 - c)) {
                    return false;
                }
            } else if (color[neighbor] == c) {
                return false; // Same color as current vertex
            }
        }
        return true;
    };
    
    for (int v = 1; v <= n; v++) {
        if (color[v] == -1) {
            if (!dfs(v, 0)) {
                return false;
            }
        }
    }
    return true;
}

// Get the two sets of a bipartite graph
pair<vector<int>, vector<int>> get_bipartite_sets() {
    vector<int> color(n + 1, -1);
    vector<int> set1, set2;
    
    // Assume graph is bipartite (check first with is_bipartite())
    function<void(int, int)> dfs = [&](int v, int c) {
        color[v] = c;
        if (c == 0) set1.push_back(v);
        else set2.push_back(v);
        
        for (int neighbor : adj[v]) {
            if (color[neighbor] == -1) {
                dfs(neighbor, 1 - c);
            }
        }
    };
    
    for (int v = 1; v <= n; v++) {
        if (color[v] == -1) {
            dfs(v, 0);
        }
    }
    
    return {set1, set2};
}

Graph Traversal Applications

DFS Applications:

  • Finding connected components
  • Cycle detection
  • Topological sorting
  • Finding strongly connected components
  • Solving maze problems
  • Finding bridges and articulation points

BFS Applications:

  • Shortest path in unweighted graphs
  • Level-order traversal
  • Finding minimum spanning tree (Prim's algorithm)
  • Bipartite graph detection
  • Web crawling
  • Social networking (degrees of separation)

Choosing Between DFS and BFS:

  • Use DFS when you need to explore all possibilities or detect cycles
  • Use BFS when you need shortest paths or level-wise processing
  • DFS uses less memory (except for recursion stack)
  • BFS guarantees shortest path in unweighted graphs

Implementation Tips

Common Pitfalls:

  • Stack overflow with deep recursion in DFS - use iterative version for large graphs
  • Forgetting to mark vertices as visited before adding to queue in BFS
  • Not handling disconnected components
  • Incorrect parent tracking in undirected graph cycle detection

Optimization Tips:

  • Use vectors instead of arrays for better performance
  • Reserve space for containers when size is known
  • Use early termination when possible
  • Consider iterative DFS for very deep graphs

CodeForces Challenge Problems

Practice graph traversal algorithms:

Medium

DFS and BFS

Practice depth-first and breadth-first search implementations.

DFS BFS
Solve Problem