Directed Graphs

Directed graphs model asymmetric relationships and dependencies. Understanding topological sorting, strongly connected components, and cycle detection is essential for many applications.

Directed Graph Fundamentals

Directed graphs (digraphs) have edges with direction, creating unique properties and algorithms different from undirected graphs. Understanding topological ordering and strong connectivity is crucial.

Key Concepts:

  • DAG: Directed Acyclic Graph - no cycles
  • Topological Order: Linear ordering respecting edge directions
  • SCC: Strongly Connected Component - mutual reachability
  • In-degree/Out-degree: Number of incoming/outgoing edges

Basic Operations


// Directed graph representation
vector<vector<int>> adj; // Adjacency list
vector<int> indegree, outdegree;

void addDirectedEdge(int u, int v) {
    adj[u].push_back(v);
    outdegree[u]++;
    indegree[v]++;
}

// Check if edge exists
bool hasEdge(int u, int v) {
    return find(adj[u].begin(), adj[u].end(), v) != adj[u].end();
}
                    

Topological Sorting

Topological sorting produces a linear ordering of vertices where for every directed edge u→v, u appears before v in the ordering. Only possible for DAGs.

Kahn's Algorithm (BFS-based)


// Kahn's algorithm for topological sorting
vector<int> topologicalSortKahn(int n, vector<vector<int>>& adj) {
    vector<int> indegree(n, 0);
    
    // Calculate indegrees
    for(int u = 0; u < n; u++) {
        for(int v : adj[u]) {
            indegree[v]++;
        }
    }
    
    queue<int> q;
    
    // Add vertices with indegree 0
    for(int i = 0; i < n; i++) {
        if(indegree[i] == 0) {
            q.push(i);
        }
    }
    
    vector<int> topoOrder;
    
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        topoOrder.push_back(u);
        
        // Remove u and decrease indegree of neighbors
        for(int v : adj[u]) {
            indegree[v]--;
            if(indegree[v] == 0) {
                q.push(v);
            }
        }
    }
    
    // Check if topological sort is possible (no cycles)
    if(topoOrder.size() != n) {
        return {}; // Cycle detected
    }
    
    return topoOrder;
}
                    

DFS-based Topological Sort


vector<int> topoOrder;
vector<int> color; // 0: white, 1: gray, 2: black
bool hasCycle = false;

void topoDFS(int u, vector<vector<int>>& adj) {
    color[u] = 1; // Mark as gray (visiting)
    
    for(int v : adj[u]) {
        if(color[v] == 1) {
            hasCycle = true; // Back edge found (cycle)
            return;
        }
        if(color[v] == 0) {
            topoDFS(v, adj);
        }
    }
    
    color[u] = 2; // Mark as black (finished)
    topoOrder.push_back(u);
}

vector<int> topologicalSortDFS(int n, vector<vector<int>>& adj) {
    color.assign(n, 0);
    topoOrder.clear();
    hasCycle = false;
    
    for(int i = 0; i < n; i++) {
        if(color[i] == 0) {
            topoDFS(i, adj);
            if(hasCycle) return {}; // Cycle detected
        }
    }
    
    reverse(topoOrder.begin(), topoOrder.end());
    return topoOrder;
}
                    

Strongly Connected Components

A strongly connected component is a maximal set of vertices where every vertex is reachable from every other vertex in the same component.

Kosaraju's Algorithm


class StronglyConnectedComponents {
private:
    int n;
    vector<vector<int>> adj, radj; // Original and reverse graph
    vector<bool> visited;
    vector<int> order, component;
    int numComponents;
    
    void dfs1(int u) {
        visited[u] = true;
        for(int v : adj[u]) {
            if(!visited[v]) {
                dfs1(v);
            }
        }
        order.push_back(u);
    }
    
    void dfs2(int u, int comp) {
        component[u] = comp;
        for(int v : radj[u]) {
            if(component[v] == -1) {
                dfs2(v, comp);
            }
        }
    }
    
public:
    StronglyConnectedComponents(int n) : n(n), adj(n), radj(n) {}
    
    void addEdge(int u, int v) {
        adj[u].push_back(v);
        radj[v].push_back(u); // Reverse edge
    }
    
    void findSCCs() {
        // Step 1: Get finishing order using DFS on original graph
        visited.assign(n, false);
        order.clear();
        
        for(int i = 0; i < n; i++) {
            if(!visited[i]) {
                dfs1(i);
            }
        }
        
        // Step 2: DFS on reverse graph in reverse finishing order
        component.assign(n, -1);
        numComponents = 0;
        
        reverse(order.begin(), order.end());
        
        for(int u : order) {
            if(component[u] == -1) {
                dfs2(u, numComponents++);
            }
        }
    }
    
    int getComponent(int u) { return component[u]; }
    int getNumComponents() { return numComponents; }
    
    vector<vector<int>> getComponentGroups() {
        vector<vector<int>> groups(numComponents);
        for(int i = 0; i < n; i++) {
            groups[component[i]].push_back(i);
        }
        return groups;
    }
    
    // Build condensation graph (DAG of SCCs)
    vector<vector<int>> getCondensationGraph() {
        vector<vector<int>> condAdj(numComponents);
        set<pair<int, int>> edges;
        
        for(int u = 0; u < n; u++) {
            for(int v : adj[u]) {
                int compU = component[u], compV = component[v];
                if(compU != compV && edges.find({compU, compV}) == edges.end()) {
                    condAdj[compU].push_back(compV);
                    edges.insert({compU, compV});
                }
            }
        }
        
        return condAdj;
    }
};
                    

Tarjan's Algorithm (Alternative)


class TarjanSCC {
private:
    int n, timer, sccCount;
    vector<vector<int>> adj;
    vector<int> low, disc, sccId;
    vector<bool> onStack;
    stack<int> st;
    
    void tarjanDFS(int u) {
        low[u] = disc[u] = timer++;
        st.push(u);
        onStack[u] = true;
        
        for(int v : adj[u]) {
            if(disc[v] == -1) {
                tarjanDFS(v);
                low[u] = min(low[u], low[v]);
            } else if(onStack[v]) {
                low[u] = min(low[u], disc[v]);
            }
        }
        
        // Root of SCC
        if(low[u] == disc[u]) {
            while(true) {
                int v = st.top();
                st.pop();
                onStack[v] = false;
                sccId[v] = sccCount;
                if(v == u) break;
            }
            sccCount++;
        }
    }
    
public:
    TarjanSCC(int n, vector<vector<int>>& graph) : n(n), adj(graph), timer(0), sccCount(0) {
        low.assign(n, -1);
        disc.assign(n, -1);
        sccId.assign(n, -1);
        onStack.assign(n, false);
        
        for(int i = 0; i < n; i++) {
            if(disc[i] == -1) {
                tarjanDFS(i);
            }
        }
    }
    
    int getComponentId(int u) { return sccId[u]; }
    int getNumComponents() { return sccCount; }
};
                    

Dynamic Programming on DAGs

DAGs enable efficient DP solutions since we can process vertices in topological order:

Longest Path in DAG


// Longest path in weighted DAG
vector<long long> longestPathDAG(int n, vector<vector<pair<int, int>>>& adj) {
    vector<int> topoOrder = topologicalSortKahn(n, convertToUnweighted(adj));
    if(topoOrder.empty()) return {}; // Not a DAG
    
    vector<long long> dist(n, LLONG_MIN);
    dist[topoOrder[0]] = 0; // Start from first vertex in topo order
    
    for(int u : topoOrder) {
        if(dist[u] == LLONG_MIN) continue;
        
        for(auto& edge : adj[u]) {
            int v = edge.first;
            int weight = edge.second;
            dist[v] = max(dist[v], dist[u] + weight);
        }
    }
    
    return dist;
}

// Count number of paths in DAG
vector<long long> countPathsDAG(int n, vector<vector<int>>& adj, int source) {
    vector<int> topoOrder = topologicalSortDFS(n, adj);
    if(topoOrder.empty()) return {}; // Not a DAG
    
    vector<long long> pathCount(n, 0);
    pathCount[source] = 1;
    
    for(int u : topoOrder) {
        for(int v : adj[u]) {
            pathCount[v] += pathCount[u];
        }
    }
    
    return pathCount;
}

// DP on DAG: Maximum sum path
long long maxSumPathDAG(int n, vector<vector<int>>& adj, vector<int>& values) {
    vector<int> topoOrder = topologicalSortDFS(n, adj);
    if(topoOrder.empty()) return LLONG_MIN; // Not a DAG
    
    vector<long long> dp(n, LLONG_MIN);
    
    // Initialize all vertices (they can be starting points)
    for(int i = 0; i < n; i++) {
        dp[i] = values[i];
    }
    
    long long maxSum = *max_element(values.begin(), values.end());
    
    for(int u : topoOrder) {
        if(dp[u] == LLONG_MIN) continue;
        
        for(int v : adj[u]) {
            dp[v] = max(dp[v], dp[u] + values[v]);
            maxSum = max(maxSum, dp[v]);
        }
    }
    
    return maxSum;
}
                    

Cycle Detection

Detecting cycles in directed graphs using DFS color coding:


// Cycle detection using DFS colors
bool hasCycleDirected(int n, vector<vector<int>>& adj) {
    vector<int> color(n, 0); // 0: white, 1: gray, 2: black
    
    function<bool(int)> dfs = [&](int u) -> bool {
        color[u] = 1; // Mark as gray
        
        for(int v : adj[u]) {
            if(color[v] == 1) return true; // Back edge (cycle)
            if(color[v] == 0 && dfs(v)) return true;
        }
        
        color[u] = 2; // Mark as black
        return false;
    };
    
    for(int i = 0; i < n; i++) {
        if(color[i] == 0 && dfs(i)) {
            return true;
        }
    }
    
    return false;
}

// Find all vertices in cycles
vector<bool> findVerticesInCycles(int n, vector<vector<int>>>& adj) {
    vector<int> color(n, 0);
    vector<bool> inCycle(n, false);
    vector<int> parent(n, -1);
    
    function<void(int, int)> dfs = [&](int u, int cycleStart) {
        color[u] = 1;
        
        for(int v : adj[u]) {
            if(color[v] == 1) {
                // Found cycle, mark all vertices from v to u
                int curr = u;
                while(curr != v) {
                    inCycle[curr] = true;
                    curr = parent[curr];
                }
                inCycle[v] = true;
            } else if(color[v] == 0) {
                parent[v] = u;
                dfs(v, cycleStart);
            }
        }
        
        color[u] = 2;
    };
    
    for(int i = 0; i < n; i++) {
        if(color[i] == 0) {
            dfs(i, -1);
        }
    }
    
    return inCycle;
}
                    

Directed Graph Applications

  • Task Scheduling: Use topological sort for dependency resolution
  • Circuit Analysis: SCCs help identify strongly connected circuits
  • Web Crawling: Find strongly connected web page clusters
  • Social Networks: Identify communities using SCC decomposition

CodeForces Challenge Problems

Practice directed graph algorithms:

Medium

Topological Sort

Practice topological sorting and dependency resolution.

Topological Sort DAG
Solve Problem