FeedbackArticles

GRAPHS

In computer science, a graph is a data structure that is used to represent the relationship between a set of objects. It consists of a set of vertices or nodes, which are connected by edges. The edges represent the relationship between the vertices, and can be directed or undirected.

Graphs are used in many applications, including computer networks, social networks, transportation networks, and more. They are particularly useful for modeling complex relationships between objects.

There are many different types of graphs, each with their own set of properties and characteristics. Some of the most common types of graphs include:

  • Directed Graphs: These are graphs in which the edges have a direction, and the relationship between vertices is one-way. They are also known as digraphs.
  • Undirected Graphs: These are graphs in which the edges do not have a direction, and the relationship between vertices is two-way.
  • Weighted Graphs: These are graphs in which each edge has a weight, which represents a cost or distance between vertices.
  • Bipartite Graphs: These are graphs in which the vertices can be divided into two sets, with edges only connecting vertices from different sets.
  • Complete Graphs: These are graphs in which each vertex is connected to every other vertex.
  • Tree: A tree is a special type of graph in which there is only one path between any two vertices, and there are no cycles.
  • Sparse Graphs: These are graphs in which the number of edges is much smaller than the number of vertices.
  • Dense Graphs: These are graphs in which the number of edges is close to the maximum possible number of edges.

Graphs can be represented in different ways, including adjacency matrices, adjacency lists, and edge lists. The choice of representation depends on the specific application and the characteristics of the graph.

ADJACENCY MATRICES

Graphs can be implemented in many different ways, and one common way is to use an adjacency matrix. In this representation, a graph with n nodes is represented by an n by n matrix where each element a[i][j] represents the weight of the edge from node i to node j. If there is no edge from node i to node j, then the element a[i][j] is set to infinity (or some other appropriate value).

If the graph is undirected, the adjacency matrix is symmetric, meaning that a[i][j] = a[j][i] for all i and j.

One advantage of using an adjacency matrix to represent a graph is that it is easy to determine whether there is an edge between two nodes i and j. You simply look at the value of a[i][j]. If it is infinity, there is no edge; otherwise, the value represents the weight of the edge.

Another advantage is that it is easy to calculate the degree of a node i. The degree is simply the sum of the weights in the row i.

However, one disadvantage of using an adjacency matrix is that it requires O(n^2) space, even if the graph is sparse (meaning that there are relatively few edges). This can make it impractical to use for large graphs.

Another disadvantage is that it can be slow to iterate over all the neighbors of a node i, since you have to examine all the elements in row i of the matrix. This can be a problem if the graph is very large or if you need to do a lot of graph traversal.

Despite these drawbacks, adjacency matrices are still a useful way to represent graphs, especially for small graphs or for algorithms that need to do a lot of edge testing or degree calculations.

Here's an example implementation of an adjacency matrix representation of a directed graph in Java:

public class Graph {

    private int[][] adjacencyMatrix;

    private int numVertices;

    public Graph(int numVertices) {

        this.numVertices = numVertices;

        this.adjacencyMatrix = new int[numVertices][numVertices];

    }

    public void addEdge(int start, int end) {

        adjacencyMatrix[start][end] = 1;

    }

    public void removeEdge(int start, int end) {

        adjacencyMatrix[start][end] = 0;

    }

    public boolean hasEdge(int start, int end) {

        return adjacencyMatrix[start][end] == 1;

    }

    public String toString() {

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < numVertices; i++) {

            for (int j = 0; j < numVertices; j++) {

                sb.append(adjacencyMatrix[i][j] + " ");

            }

            sb.append("\n");

        }

        return sb.toString();

    }

}

In this implementation, the graph is represented by a two-dimensional integer array, where the value at adjacencyMatrix[i][j] represents the weight of the edge from vertex i to vertex j. If there is no edge between vertices i and j, the value at that position is 0.

The addEdge, removeEdge, and hasEdge methods allow you to modify the edges of the graph. addEdge adds an edge from the vertex start to the vertex end. removeEdge removes an edge between those vertices. hasEdge returns true if there is an edge between the vertices and false otherwise.

The toString method allows you to print out the adjacency matrix for the graph. It loops through each position in the matrix and appends the value at that position to a string, along with a space. When it reaches the end of a row, it adds a newline character to move to the next row.

ADJACENCY LISTS

Graphs can also be implemented using adjacency lists. In this implementation, each vertex in the graph has a list of adjacent vertices.

The adjacency list implementation uses less space than the adjacency matrix implementation, especially for sparse graphs. It also allows for efficient iteration over the neighbors of a vertex.

To implement a graph using adjacency lists, we can create a class for the vertices that contains the vertex value and a list of its neighbors. Then, we can create a graph class that contains a list of vertices.

Here is an example implementation of a graph using adjacency lists in Java:

import java.util.ArrayList;

import java.util.List;

public class Graph {

    private List<Vertex> vertices;

    public Graph() {

        vertices = new ArrayList<>();

    }

    public void addVertex(Vertex v) {

        vertices.add(v);

    }

    public void addEdge(Vertex v, Vertex u) {

        v.addNeighbor(u);

        u.addNeighbor(v);

    }

    public List<Vertex> getVertices() {

        return vertices;

    }

    public static class Vertex {

        private int value;

        private List<Vertex> neighbors;

        public Vertex(int value) {

            this.value = value;

            neighbors = new ArrayList<>();

        }

        public int getValue() {

            return value;

        }

        public List<Vertex> getNeighbors() {

            return neighbors;

        }

        public void addNeighbor(Vertex v) {

            neighbors.add(v);

        }

    }

}

In this implementation, each vertex is represented by the Vertex class, which contains the vertex value and a list of its neighbors. The graph is represented by the Graph class, which contains a list of vertices. The addVertex method adds a vertex to the graph, and the addEdge method adds an undirected edge between two vertices. The getVertices method returns the list of vertices in the graph.

In the above table, V is the number of vertices in the graph, E is the number of edges in the graph, and degree(v) is the number of edges connected to vertex v.

In general, adjacency lists are a more efficient way to represent sparse graphs, where the number of edges is much smaller than the number of possible edges. The worst case time complexity for removing a vertex or edge is O(E), but this is only a concern for dense graphs. The average case time complexity for most operations is O(1), making adjacency lists a good choice for most graph problems.

EDGE LISTS

In a graph, an edge list is a data structure that contains all of the edges in the graph. Each edge is represented as a tuple (u, v, w), where u and v are the vertices that the edge connects and w is the weight of the edge (if the graph is weighted).

An edge list can be useful for certain types of graph algorithms, such as Kruskal's algorithm for finding a minimum spanning tree, as it allows for quick access to all of the edges in the graph.

However, an edge list is not an efficient data structure for many other types of graph algorithms, such as breadth-first search or depth-first search, as it does not provide a quick way to access all of the neighbors of a particular vertex. For those types of algorithms, an adjacency list or adjacency matrix is typically used instead.

Here's an example implementation of an edge list in Java:

public class Edge {

    private int source;

    private int destination;

    private int weight;

    public Edge(int source, int destination) {

        this(source, destination, 0);

    }

    public Edge(int source, int destination, int weight) {

        this.source = source;

        this.destination = destination;

        this.weight = weight;

    }

    public int getSource() {

        return source;

    }

    public int getDestination() {

        return destination;

    }

    public int getWeight() {

        return weight;

    }

}

public class EdgeListGraph {

    private List<Edge> edges;

    private int numVertices;

    public EdgeListGraph(int numVertices) {

        this.numVertices = numVertices;

        this.edges = new ArrayList<>();

    }

    public void addEdge(int source, int destination) {

        addEdge(source, destination, 0);

    }

    public void addEdge(int source, int destination, int weight) {

        Edge edge = new Edge(source, destination, weight);

        edges.add(edge);

    }

    public List<Edge> getEdges() {

        return edges;

    }

    public int getNumVertices() {

        return numVertices;

    }

}

In this implementation, we have an Edge class that represents an individual edge, and a EdgeListGraph class that represents the graph as a whole. The EdgeListGraph class contains a list of edges and the number of vertices in the graph. It also has methods for adding edges and getting the list of edges and number of vertices.

DEPTH-FIRST SEARCH

Depth-first search (DFS) is a graph traversal algorithm used to explore the nodes and edges of a graph. The algorithm starts at a source vertex, and then visits all the vertices that are reachable from it through a series of deepening steps, until it reaches the end of a branch.

The algorithm works by visiting the source vertex and then recursively visiting all of its unvisited neighbors, repeating the process for each new vertex visited. When a vertex is visited, it is marked as visited to prevent the algorithm from revisiting it later.

DFS can be implemented using a stack or recursion to keep track of the vertices that have been visited and those that still need to be visited.

DFS can be used to solve various problems related to graph traversal, such as finding a path between two vertices, checking if a graph is connected, and identifying cycles in a graph.

Here's a Java implementation of depth-first search on a graph represented by an adjacency list:

import java.util.*;

public class Graph {

    private int numVertices;

    private List<List<Integer>> adjList;

    public Graph(int numVertices) {

        this.numVertices = numVertices;

        this.adjList = new ArrayList<>(numVertices);

        for (int i = 0; i < numVertices; i++) {

            this.adjList.add(new ArrayList<>());

        }

    }

    public void addEdge(int u, int v) {

        this.adjList.get(u).add(v);

        this.adjList.get(v).add(u);

    }

    public List<Integer> depthFirstSearch(int start) {

        List<Integer> visited = new ArrayList<>();

        boolean[] marked = new boolean[this.numVertices];

        dfs(start, marked, visited);

        return visited;

    }

    private void dfs(int v, boolean[] marked, List<Integer> visited) {

        marked[v] = true;

        visited.add(v);

        for (int neighbor : this.adjList.get(v)) {

            if (!marked[neighbor]) {

                dfs(neighbor, marked, visited);

            }

        }

    }

}

Here, Graph is a class that represents an undirected graph. The constructor takes an integer that represents the number of vertices in the graph, and initializes an empty adjacency list with the specified number of vertices. The addEdge method takes two integers u and v, and adds an undirected edge between vertices u and v.

The depthFirstSearch method takes an integer start that represents the starting vertex for the search. It initializes an empty visited list, a boolean array marked that tracks which vertices have been visited, and calls the private dfs method to perform the actual search. The dfs method takes a vertex v, the marked array, and the visited list as input. It marks vertex v as visited, adds it to the visited list, and recursively calls itself on each unmarked neighbor of v.

This implementation uses an adjacency list to represent the graph, and performs a depth-first search on the graph starting at the specified vertex. The visited list returned by the depthFirstSearch method contains the vertices of the graph in the order they were visited during the search.

BREADTH-FIRST SEARCH

Breadth First Search (BFS) is an algorithm used to traverse or search through a graph or tree. The basic idea behind BFS is to explore all the nodes at a particular level before moving to the next level. In other words, it starts from the root node, then moves to its children, then to the children of its children, and so on. BFS uses a queue data structure to keep track of the nodes to be visited.

Here is the algorithm for BFS:

  1. Create a queue and enqueue the starting node.
  2. Mark the starting node as visited.
  3. While the queue is not empty, do the following:
    1. Dequeue a node from the queue.
    2. Visit the dequeued node and process it.
    3. Enqueue all the unvisited adjacent nodes of the dequeued node.
    4. Mark all the adjacent nodes as visited.

The time complexity of BFS is O(V + E), where V is the number of vertices and E is the number of edges in the graph.

Here is an example of BFS implemented in Java using an adjacency list:

import java.util.*;

public class Graph {

    private int V;

    private LinkedList<Integer>[] adj;

    public Graph(int V) {

        this.V = V;

        adj = new LinkedList[V];

        for (int i = 0; i < V; i++) {

            adj[i] = new LinkedList<Integer>();

        }

    }

    public void addEdge(int v, int w) {

        adj[v].add(w);

    }

    public void BFS(int s) {

        boolean[] visited = new boolean[V];

        LinkedList<Integer> queue = new LinkedList<Integer>();

        visited[s] = true;

        queue.add(s);

        while (queue.size() != 0) {

            s = queue.poll();

            System.out.print(s + " ");

            Iterator<Integer> i = adj[s].listIterator();

            while (i.hasNext()) {

                int n = i.next();

                if (!visited[n]) {

                    visited[n] = true;

                    queue.add(n);

                }

            }

        }

    }

}

In this example, the Graph class represents an undirected graph using an adjacency list. The addEdge method is used to add an edge between two vertices. The BFS method performs a breadth first search starting from a given vertex s. It uses a visited array to keep track of which vertices have been visited and a queue to store the vertices to be visited. The while loop continues until the queue is empty, dequeuing the next vertex to visit and adding its unvisited neighbors to the queue.

KRUSKAL ALGORITHM

Kruskal's algorithm is a greedy algorithm that finds the minimum spanning tree of a connected, weighted graph. Given a connected, undirected, and weighted graph, the minimum spanning tree is a tree that connects all vertices in the graph with the minimum possible total edge weight.

Kruskal's algorithm works as follows:

  1. Sort the edges of the graph in non-decreasing order of weight.
  2. Create a forest where each vertex is a separate tree.
  3. Iterate over the sorted edges and for each edge:
    1. If the edge connects two vertices from different trees, add it to the forest and merge the trees.
    2. If the edge connects two vertices from the same tree, discard it.
  4. Stop when all vertices are in the same tree (i.e., there is only one tree in the forest).

Kruskal's algorithm has a time complexity of O(E log E), where E is the number of edges in the graph. The space complexity is O(V), where V is the number of vertices in the graph.

PRIM ALGORITHM

Prim's algorithm is a greedy algorithm that finds a minimum spanning tree for a weighted undirected graph. The algorithm operates by building the tree one vertex at a time, starting from an arbitrary vertex, and maintaining a set of visited vertices and a set of frontier vertices. At each iteration, the algorithm selects the frontier vertex with the smallest edge weight to add to the visited vertices set, and updates the frontier vertices set with the new vertices that are adjacent to the visited vertices.

Here is the general outline of Prim's algorithm:

  1. Initialize an empty set visited to store the vertices that have been visited so far, and a set frontier to store the vertices that are adjacent to the visited vertices but have not been visited yet.
  2. Select an arbitrary starting vertex s and add it to visited.
  3. For each vertex v adjacent to s, add the edge (s, v) to frontier.
  4. While frontier is not empty, select the edge (u, v) with the smallest weight from frontier.
  5. If v has not been visited yet, add v to visited and add all edges that are adjacent to v to frontier.
  6. Repeat steps 4-5 until all vertices have been visited.

DIJKSTRA’S ALGORITHM

Dijkstra's algorithm is a graph algorithm used to find the shortest path between two nodes in a graph with non-negative edge weights. It starts at the source node and iteratively finds the next node with the shortest distance to the source until it finds the destination node or there are no more nodes to explore.

The algorithm maintains a set of unvisited nodes and a set of tentative distances from the source to each node. Initially, the tentative distance for the source node is set to 0 and the tentative distances for all other nodes are set to infinity. At each step, the algorithm selects the unvisited node with the smallest tentative distance and updates the tentative distances of its neighbors, if necessary. The selected node is then marked as visited.

The algorithm terminates when either the destination node is visited or all nodes have been visited. The shortest path from the source to the destination can be obtained by backtracking from the destination node to the source node along the path with the smallest tentative distances.

Dijkstra's algorithm can be implemented using a priority queue to efficiently select the node with the smallest tentative distance at each step. The time complexity of the algorithm is O((E+V)logV) where E is the number of edges and V is the number of vertices in the graph.

  1. Initialize the graph: Set up the graph with vertices and edges, along with their associated weights or distances. Assign a starting vertex as the source.
  2. Set initial distances: Assign a distance of 0 to the source vertex and infinity to all other vertices. Keep track of the shortest distance from the source to each vertex in a distance table.
  3. Create a priority queue: Create a priority queue to store the vertices based on their tentative distances from the source. The priority queue should prioritize vertices with smaller distances.
  4. Process vertices: While the priority queue is not empty, do the following steps:
    1. Extract the vertex with the smallest distance from the priority queue. This vertex will be the current vertex.
    2. Visit each neighboring vertex of the current vertex. Calculate the tentative distance from the source to each neighboring vertex by summing up the distance of the current vertex and the weight of the edge connecting them.
    3. If the tentative distance is smaller than the current recorded distance in the distance table, update the distance table with the new smaller distance.
    4. If the neighboring vertex is not already visited, add it to the priority queue.
  5. Repeat step 4 until all vertices have been visited or until the destination vertex (if specified) is reached.
  6. Retrieve the shortest path: Once the algorithm finishes, the shortest path from the source to any vertex can be obtained by backtracking through the recorded distances and edges from the destination vertex (or any desired vertex).

BELLMAN-FORD ALGORITHM

Bellman-Ford algorithm is a single-source shortest path algorithm that works with weighted graphs, which may contain negative weight edges. It is slower than Dijkstra's algorithm, but it can handle graphs with negative edge weights. The algorithm keeps a list of shortest distances to each vertex and updates these distances if a shorter path is found. The algorithm works by iterating over all the edges in the graph for a number of times equal to the number of vertices, and updating the distances for each vertex.

Here's a brief outline of the algorithm:

  1. Set the distance of the source vertex to 0, and the distances of all other vertices to infinity.
  2. Repeat the following steps for V-1 times, where V is the number of vertices in the graph:a. Iterate over all the edges in the graph and update the distance of the destination vertex if the sum of the distance of the source vertex and the weight of the edge is smaller than the current distance of the destination vertex.
  3. Check for negative weight cycles by iterating over all the edges in the graph one more time, and if a shorter path is found, then there is a negative weight cycle.

If there is no negative weight cycle, then the algorithm returns the list of shortest distances to each vertex. If there is a negative weight cycle, then the algorithm detects it and returns an error.

The time complexity of the Bellman-Ford algorithm is O(VE), where V is the number of vertices and E is the number of edges in the graph.

FLOYD-WARSHALL ALGORITHM

The Floyd-Warshall algorithm is a dynamic programming algorithm that computes the shortest paths between all pairs of vertices in a weighted directed graph. It does so by maintaining a matrix of shortest path distances and updates this matrix as it processes the graph.

The algorithm works by iterating over all possible intermediate vertices and checking if there is a path between each pair of vertices that goes through the intermediate vertex. If such a path is found, and it is shorter than the current known path between the two vertices, then the shortest path distance is updated.

Here are the steps of the Floyd-Warshall algorithm:

  1. Initialize a matrix dist of size n x n, where n is the number of vertices in the graph. For each pair (i, j), dist[i][j] represents the shortest distance between vertex i and vertex j.
  2. For each vertex i, set dist[i][i] = 0.
  3. For each edge (i, j) with weight w, set dist[i][j] = w.
  4. For each intermediate vertex k from 1 to n, iterate over each pair of vertices i and j. If dist[i][j] > dist[i][k] + dist[k][j], then update dist[i][j] = dist[i][k] + dist[k][j].
  5. The final matrix dist contains the shortest path distances between all pairs of vertices.

The time complexity of the Floyd-Warshall algorithm is O(n^3), where n is the number of vertices in the graph. This makes it suitable for small graphs, but it can become impractical for larger graphs.

PATTERNS

SHORTEST PATH

The shortest path pattern is a common pattern used in graph algorithms to find the shortest path between two nodes in a graph. The problem involves finding the shortest path between two nodes in a graph, where each edge has a non-negative weight.

The pattern typically involves using a variation of Dijkstra's algorithm, which is a popular algorithm for solving the shortest path problem. Dijkstra's algorithm works by starting at the source node, and iteratively updating the distance of all the neighboring nodes until the destination node is reached.

The shortest path pattern can also be used in conjunction with other graph algorithms, such as A* search and Bellman-Ford algorithm, depending on the specific requirements of the problem.

Common applications of the shortest path pattern include routing algorithms, path-finding algorithms in video games, and navigation systems.

MINIMUM SPANNING TREE

The Minimum Spanning Tree (MST) pattern is a common pattern in graph theory that involves finding a subgraph of a weighted, connected graph that connects all vertices together with the minimum total edge weight. The resulting subgraph is known as a minimum spanning tree. The MST pattern is used in a variety of applications, including network design, clustering, and data compression.

The most well-known algorithm for finding the MST of a graph is Kruskal's algorithm, which builds the MST by adding edges in increasing order of weight, and never adding an edge that would create a cycle. Another common algorithm is Prim's algorithm, which builds the MST by starting at an arbitrary vertex and adding edges that connect the current tree to the nearest non-tree vertex.

Once the MST has been computed, it can be used to find the shortest path between any two vertices in the graph. This is because any path between two vertices that is not part of the MST must be longer than the shortest path that is part of the MST. This property is used in a number of applications, such as network routing and clustering.

In general, the MST pattern is a powerful tool for finding the optimal subgraph of a graph that connects all vertices. Its applications are numerous and diverse, and it is a fundamental concept in graph theory.

TOPOLOGICAL SORT

The topological sort pattern is used to order items or tasks that have dependencies in such a way that every item or task is placed after all of its dependencies. It can be applied to a wide range of problems, including project scheduling, software dependency management, and task ordering.

In this pattern, a directed graph is used to represent the dependencies between the items or tasks. The topological sort algorithm then processes the graph and produces a linear ordering of the vertices that satisfies the dependencies.

The basic idea behind the topological sort algorithm is to use a depth-first search to explore the graph, keeping track of the visited vertices and their state (visited, unvisited, or in progress). When a vertex has all of its dependencies satisfied, it can be added to the output list. The algorithm continues until all of the vertices have been visited.

The topological sort algorithm can be implemented using either a recursive or iterative approach. The recursive approach is simple to understand and easy to implement, but can suffer from issues with large graphs and potential stack overflow errors. The iterative approach is more complex, but is generally faster and more memory efficient.

The topological sort pattern can be useful in a wide range of applications. For example, it can be used to order tasks in a project management system, to optimize the order of database table creation, or to ensure that code is compiled in the correct order in a software build system.

Here's an example implementation of topological sort in Java using the adjacency list representation of a graph:

public List<Integer> sort(int numVertices, int[][] edges) {

        List<Integer> result = new ArrayList<>();

        // Build the graph using an adjacency list

        Map<Integer, List<Integer>> graph = new HashMap<>();

        int[] inDegree = new int[numVertices];

        for (int i = 0; i < numVertices; i++) {

            graph.put(i, new ArrayList<>());

        }

        for (int[] edge : edges) {

            int u = edge[0];

            int v = edge[1];

            graph.get(u).add(v);

            inDegree[v]++;

        }

        // Perform topological sort using a queue

        Queue<Integer> queue = new LinkedList<>();

        for (int i = 0; i < numVertices; i++) {

            if (inDegree[i] == 0) {

                queue.offer(i);

            }

        }

        while (!queue.isEmpty()) {

            int u = queue.poll();

            result.add(u);

            for (int v : graph.get(u)) {

                inDegree[v]--;

                if (inDegree[v] == 0) {

                    queue.offer(v);

                }

            }

        }

        // Check if there is a cycle in the graph

        if (result.size() != numVertices) {

            return new ArrayList<>();

        }

        return result;

  }

The sort method takes in the number of vertices in the graph and a list of edges represented as a 2D integer array. It returns a list of integers representing the order in which the vertices can be visited in a topological sort.

The implementation first builds the graph using an adjacency list representation and an array to keep track of the in-degree of each vertex. It then performs a topological sort using a queue, starting with the vertices that have an in-degree of 0. The implementation checks for the presence of a cycle in the graph by comparing the size of the resulting list with the number of vertices in the graph.

This implementation has a time complexity of O(V + E), where V is the number of vertices and E is the number of edges in the graph, as we need to visit each vertex and edge once.

CONNECTED COMPONENTS

The Connected Components pattern is a technique used in graph problems to group vertices into subsets such that every vertex in a subset is connected to at least one other vertex in the same subset.

The pattern is based on the idea that we can traverse the graph using depth-first search (DFS) or breadth-first search (BFS) to identify all the vertices that belong to a connected component. Starting from any vertex, we can explore all reachable vertices in a connected component and mark them as visited.

The algorithm for finding connected components in an undirected graph using DFS is as follows:

  1. Initialize a visited array with all values set to false.
  2. For each unvisited vertex, perform DFS to mark all the connected vertices as visited and add them to a connected component.
  3. Repeat step 2 until all vertices have been visited.

After performing DFS, we would have identified all the connected components in the graph. Each connected component can be represented as a separate set of vertices.

Here's an example implementation in Java using DFS:

public List<Set<Integer>> findConnectedComponents(int n, List<List<Integer>> edges) {

    List<Set<Integer>> components = new ArrayList<>();

    boolean[] visited = new boolean[n];

    for (int i = 0; i < n; i++) {

        if (!visited[i]) {

            Set<Integer> component = new HashSet<>();

            dfs(i, visited, component, edges);

            components.add(component);

        }

    }

    return components;

}

private void dfs(int vertex, boolean[] visited, Set<Integer> component, List<List<Integer>> edges) {

    visited[vertex] = true;

    component.add(vertex);

   

    for (int neighbor : edges.get(vertex)) {

        if (!visited[neighbor]) {

            dfs(neighbor, visited, component, edges);

        }

    }

}

In this implementation, the findConnectedComponents method takes in the number of vertices n and a list of edges edges as input, and returns a list of sets where each set represents a connected component.

The DFS method dfs takes in a vertex, a visited array, a set to add vertices to, and the list of edges. It recursively performs DFS to add all vertices in the connected component to the set, marking them as visited in the visited array as it goes.

This pattern can be used to solve a variety of problems, such as finding the number of islands in a matrix or grouping people in a social network by their connectedness.

STRONGLY CONNECTED COMPONENTS

The Strongly Connected Components (SCC) pattern is used to identify groups of vertices in a directed graph that are mutually reachable. In other words, SCCs are groups of vertices where every vertex in the group can reach every other vertex in the group, and no vertex outside the group can reach any vertex inside the group.

The Tarjan's algorithm is a commonly used algorithm to find strongly connected components in a directed graph. It performs a depth-first search on the graph, while maintaining information about the vertices it has visited, the order in which they were visited, and the depth of each vertex in the search tree. The algorithm uses a stack to keep track of the vertices that are currently being explored.

During the search, Tarjan's algorithm assigns a "low-link" value to each vertex. This value represents the smallest depth at which the vertex can be reached in the search. If a vertex has a low-link value that is equal to its own depth, then it is the root of a strongly connected component.

Once the search is complete, the algorithm can identify the strongly connected components by looking at the low-link values of the vertices. Specifically, each vertex with a low-link value equal to its own depth is the root of a strongly connected component, and the vertices that were visited before it on the stack (i.e., the vertices with lower depth) belong to the same component.

Here is a Java implementation of Tarjan's algorithm:

public class StronglyConnectedComponents {

    private int index;

    private int[] lowLink;

    private boolean[] onStack;

    private Stack<Integer> stack;

    private List<List<Integer>> sccs;

    public List<List<Integer>> findSCCs(int[][] graph) {

        int n = graph.length;

        index = 0;

        lowLink = new int[n];

        Arrays.fill(lowLink, -1);

        onStack = new boolean[n];

        stack = new Stack<>();

        sccs = new ArrayList<>();

        for (int v = 0; v < n; v++) {

            if (lowLink[v] == -1) {

                dfs(graph, v);

            }

        }

        return sccs;

    }

    private void dfs(int[][] graph, int v) {

        lowLink[v] = index;

        index++;

        onStack[v] = true;

        stack.push(v);

        for (int w : graph[v]) {

            if (lowLink[w] == -1) {

                dfs(graph, w);

                lowLink[v] = Math.min(lowLink[v], lowLink[w]);

            } else if (onStack[w]) {

                lowLink[v] = Math.min(lowLink[v], lowLink[w]);

            }

        }

        if (lowLink[v] == index - 1) {

            List<Integer> scc = new ArrayList<>();

            int w;

            do {

                w = stack.pop();

                onStack[w] = false;

                scc.add(w);

            } while (w != v);

            sccs.add(scc);

        }

    }

}

This implementation takes an adjacency matrix as input and returns a list of lists, where each inner list represents a strongly connected component. The algorithm has a time complexity of O(V+E), where V is the number of vertices and E is the number of edges in the graph.

BIPARTITE GRAPHS

The bipartite graph pattern is a graph algorithm pattern that deals with identifying if a given graph is bipartite or not. A bipartite graph is a graph whose vertices can be divided into two sets, such that all edges connect a vertex in one set to a vertex in the other set. In other words, a graph is bipartite if it is possible to color all vertices of the graph with two colors in such a way that no two adjacent vertices have the same color.

The bipartite graph pattern can be solved using graph coloring algorithms. The idea is to start from an arbitrary vertex, and color it with one color. Then, all its neighbors are colored with the other color. This process is continued until all vertices are colored or a conflict is found. If a conflict is found, i.e., two adjacent vertices have the same color, then the graph is not bipartite. If all vertices are colored without any conflict, then the graph is bipartite.

One popular algorithm to solve this problem is the two-coloring algorithm, also known as the bi-partition algorithm. The algorithm starts by selecting an arbitrary vertex, coloring it with one color, and then coloring all its neighbors with the opposite color. The algorithm then continues by selecting a vertex that has not been colored, coloring it with the first color, and coloring all its neighbors with the opposite color. This process continues until all vertices have been colored or a conflict is found.

Another algorithm for solving the bipartite graph pattern is the breadth-first search algorithm. The algorithm starts by selecting an arbitrary vertex and coloring it with one color. The algorithm then performs a breadth-first search, coloring all the vertices that are reachable from the starting vertex with the opposite color. If a conflict is found, i.e., two adjacent vertices have the same color, then the graph is not bipartite. If all vertices are colored without any conflict, then the graph is bipartite.

The bipartite graph pattern has applications in many areas, such as computer science, biology, and social networks.

PROBLEMS

Easy

  1. Clone Graph
  2. Course Schedule

Medium

  1. Number of Islands
  2. Word Ladder
  3. Network Delay Time
  4. Course Schedule II
  5. Cheapest Flights Within K Stops
  6. Alien Dictionary
  7. Pacific Atlantic Water Flow

Hard

  1. Shortest Path in a Grid with Obstacles Elimination
  2. The Maze II

SEE ALSO