Skip to main content

Recap on BFS, DFS and Strongly Connected Components

graph

graph

DFS

topological sort

considering the dependencies between tasks, we can use topological sort to find the order of tasks.

def topological_sort(graph):
    visited = set()
    stack = []
    for node in graph:
        if node not in visited:
            dfs(graph, node, visited, stack)
    return stack[::-1]

def dfs(graph, node, visited, stack):
    visited.add(node)
    for neighbor in graph[node]:
        if neighbor not in visited:
            dfs(graph, neighbor, visited, stack)
    stack.append(node)

if A -> B then B.finishtime < A.finishtime, that's why we need to reverse the stack.

Topological Sorting on a DAG

  • do DFS on the graph
  • when a node is finished, insert it into the front of a linked list

Another use of DFS is in-order traversal of a binary tree .

BFS

BFS is used to find the shortest path in an unweighted graph.

we can find the shortest path between u and v in time O(m), where m is the number of edges.

Another application of BFS: testing bipartiteness of a graph.

  • color the levels of the BFS tree in alternating colors

  • if there is an edge between two nodes of the same color, then the graph is not bipartite

  • if there is no such edge, then the graph is bipartite

If BFS colors two neighbors of a node with the same color, then it's found an cycle of odd length.

Finding Strongly Connected Components

when using you cannot reach everything using DFS, run DFS repeatedly to get a depth-first forest.

if v is a descendent of w in the same tree:

  • w.start < v.start < v.finish < w.finish

if neither v nor w is a descendent of the other:

  • v.start < w.start < w.finish < v.finish

or the other way around.

A directed graph G=(V,E)G = (V,E) is strongly connected if:

for all v, w in V:

- there is a path from v to w

- there is a path from w to v

We can decompose a graph into strongly connected components (SCCs).

Definition by definition: The SCCs are the equivalence classes under the "are mutually reachable" equivalence relation.

Why do we care about SCCs?

  • SCCs are the "building blocks" of a directed graph

  • SCCs are the "maximal" subgraphs that are "mutually reachable"

  • Strongly connected components tell you about communities.

  • Lots of graph algorithms only work on SCCs.

Kosaraju's Algorithm

  • run DFS on GG, keeping track of the finish times

  • run DFS on GTG^T, in the order of decreasing finish times

  • each tree in the second DFS is a SCC

properties of SCCs

  • The SCCs of a graph form a DAG

    • if not, then there is a cycle in the graph, and two nodes in the cycle are mutually reachable

Starting and finishing times in a SCC

  • The finishing time of a SCC is the largest finishing time of any element of that SCC.

  • The starting time of a SCC is the smallest starting time of any element of that SCC.

Finishing times allowed us to topologically sort of the vertices, it works out for SCCs as well.

For SCCs A and B if we have A -> B, then we have B.finishtime < A.finishtime.

if there is an edge A <- B in the reversed graph, then B.finishtime < A.finishtime.

C++ inplementation of Kosaraju's Algorithm (copilot auto generated):

#include <bits/stdc++.h>
using namespace std;

class Graph {
    int V;
    list<int> *adj;
    void fillOrder(int s, bool visited[], stack<int> &Stack);
    void DFSUtil(int s, bool visited[]);

    public:
        Graph(int V);
        void addEdge(int v, int w);
        void printSCCs();
        Graph getTranspose();
};

Graph::Graph(int V) {
    this->V = V;
    adj = new list<int>[V];
}

void Graph::DFSUtil(int s, bool visitedV[]{
    visited[s] = true;
    cout << s << " ";
    list<int>::iterator i;
    for (i = adj[s].begin(); i != adj[s].end(); ++i) {
        if (!visited[*i]) {
            DFSUtil(*i, visited);
        }
    }
})

Graph Graph::getTranspose(){
    Graph g(V);
    for (int s = 0; s < V; s++) {
        list<int>::iterator i;
        for (i = adj[s].begin(); i != adj[s].end(); ++i) {
            g.adj[*i].push_back(s);
        }
    }
    return g;
}

void Graph::addEdge(int v, int w) {
    adj[v].push_back(w);
}

void Graph::fillOrder(int s, bool visited[], stack<int> &Stack) {
    visited[s] = true;
    list<int>::iterator i;
    for (i = adj[s].begin(); i != adj[s].end(); ++i) {
        if (!visited[*i]) {
            fillOrder(*i, visited, Stack);
        }
    }
    Stack.push(s);
}

void Graph::printSCCs() {
    stack<int> Stack;
    bool *visited = new bool[V];
    for (int i = 0; i < V; i++) {
        visited[i] = false;
    }
    for (int i = 0; i < V; i++) {
        if (!visited[i]) {
            fillOrder(i, visited, Stack);
        }
    }
    Graph gr = getTranspose();
    for (int i = 0; i < V; i++) {
        visited[i] = false;
    }
    while (!Stack.empty()) {
        int s = Stack.top();
        Stack.pop();
        if (!visited[s]) {
            gr.DFSUtil(s, visited);
            cout << endl;
        }
    }
}

int main() {
    Graph g(5);
    g.addEdge(1, 0);
    g.addEdge(0, 2);
    g.addEdge(2, 1);
    g.addEdge(0, 3);
    g.addEdge(3, 4);
    cout << "SCCs in the graph are: " << endl;
    g.printSCCs();
    return 0;
}

Comments

Popular posts from this blog

Tries

Tries Tries Tries and Patricia Tries Motivation: The autocomplete feature in a text editor. We have a dictionary of words, and we want to find all the words that start with a given prefix. The data structure we use to store the dictionary is called a trie. Each edge is labeled with a character. Some nodes are marked as terminal nodes. By convention, children of a node are ordered alphabetically. A preordering traversal of the trie yields the words in the dictionary in alphabetical order. The total length of our text strings is m and the length of our pattern string is n . Time to build the trie: O(m) Time to search for a pattern: O(n) How long does it take to find all the words that start with a given prefix? The $ symbol is called the sentinel or marker, add this to the end of each word. It is used to mark the end of a word, it precedes all other characters in the alphabet....

Balanced Trees

Balanced Trees Balanced Trees Balanced search trees are among the most useful and versatile data structures. Many programming languages include balanced search trees in their standard libraries. e.g. C++: std::map , Java: TreeMap , Python: dict , etc. Many advanced data structures are layered on top of balanced search trees.. A quick BST recap A binary search tree is a binary tree where each node has a key and a value, and the following properties hold: Each node in the BST stores a key, and optinally, some information associated with the key. The keys in the left subtree are less than the key in its parent node, and the keys in the right subtree are greater than the key in its parent node. The height of a BST is the number of nodes on the longest path from the root to a leaf. A tree with one node has height 0, A tree with no nodes has height -1. Deleting from a BST Deleting a n...