Cpunit Iv
Cpunit Iv
Syllabus:
Graphs Algorithms: Connected Components in a graph, Finding Bridges in a Graph and Finding
Articulation Point in a Graph, Maximum Flow Algorithms, Lowest Common Ancestor.
Graphs Algorithms:
Applications:
Explanation: There are 2 different connected components. They are {0, 1, 2} and {3, 4}.
In this section, we’ll discuss a DFS-based algorithm that gives us the number of connected
components for a given undirected graph:
ConnectedComponents.java
import java.util.*;
class ConnectedComponents
{
// A graph is an array of adjacency lists.
// Size of array will be V (number of vertices in graph)
int V;
ArrayList<ArrayList<Integer> > adjListArray;
// constructor
ConnectedComponents(int V)
{
this.V = V;
// define the size of array as number of vertices
}
Sample input & output:
enter number of vertices
5
enter number of edges
3
enter edges
1
0
2
1
3
4
Following are connected components
012
34
Given an undirected graph of V vertices and E edges. Our task is to find all the bridges in the
given undirected graph.
A bridge in any graph is defined as an edge which, when removed, makes the graph
disconnected (or more precisely, increases the number of connected components in the
graph).
The algorithm described here is based on DFS and has O(N+M) complexity, where N is the
number of vertices and M is the number of edges in the graph.
For Example: If the given graph is :
The edge between 0 and 4 is the bridge because if the edge between 0 and 4 is removed, then
there will be no path left to reach from 0 to 4.and makes the graph disconnected, and
increases the number of connected components.
Note :
There are no self-loops(an edge connecting the vertex to itself) in the given graph.
There are no parallel edges i.e no two vertices are directly connected by more than 1
edge.
Sample Input-1:
---------------
4 3 //No. of nodes =4 {1,2,3,4} & No of Edges=3
12
13
24
1 //P value
Sample Output-1:
----------------
2
Explanation:
------------
There is only one path 1->2->4. so answer = 2
1. First, we need to create the adjacency list for the given graph from the edge information(If
not already given). And we will declare a variable timer(either globally or we can carry it
while calling DFS), that will keep track of the time of insertion for each node.
2. Then we will start DFS from node 0(assuming the graph contains a single component
otherwise, we will call DFS for every component) with parent -1.
2.1. Inside DFS, we will first mark the node visited and then store the time of insertion and
the lowest time of insertion properly. The timer may be initialized to 0 or 1.
2.2. Now, it’s time to visit the adjacent nodes.
2.2.1. If the adjacent node is the parent itself, we will just continue to the next node.
2.2.2.If the adjacent node is not visited, we will call DFS for the adjacent node with the
current node as the parent.
After the DFS gets completed, we will compare the lowest time of insertion of the
current node and the adjacent node and take the minimum one.
Now, we will check if the lowest time of insertion of the adjacent node is greater than
the time of insertion of the current node.
If it is, then we will store the adjacent node and the current node in our answer array as
they are representing the bridge.
2.2.3.If the adjacent node is already visited, we will just compare the lowest time of
insertion of the current node and the adjacent node and take the minimum one.
FindingBridges.java
import java.io.*;
import java.util.*;
import java.util.LinkedList;
class FindingBridges
{
private int V; // No. of vertices
// Constructor @SuppressWarnings("unchecked")
FindingBridges (int v)
{
V = v;
adj = new LinkedList[v];
for (int i=0; i<v; ++i)
adj[i] = new LinkedList();
}
// A recursive function that finds and prints bridges // using DFS traversal
// u --> The vertex to be visited next
// visited[] --> keeps track of visited vertices
// disc[] --> Stores discovery times of visited vertices
// parent[] --> Stores parent vertices in DFS tree
void bridgeUtil(int u, boolean visited[], int disc[], int low[], int parent[])
{
// Mark the current node as visited
visited[u] = true;
// If v is not visited yet, then make it a child of u in DFS tree and recur for it.
// If v is not visited yet, then recur for it
if (!visited[v])
{
parent[v] = u;
bridgeUtil(v, visited, disc, low, parent);
// Check if the subtree rooted with v has a connection to one of the ancestors of u
void bridge()
{
// Mark all the vertices as not visited
boolean visited[] = new boolean[V];
int disc[] = new int[V];
int low[] = new int[V];
int parent[] = new int[V];
// Call the recursive helper function to find Bridges in DFS tree rooted with vertex 'i'
for (int i = 0; i < V; i++)
if (visited[i] == false)
bridgeUtil(i, visited, disc, low, parent);
}
}
}
Sample input & output:
enter number of vertices
5
enter number of edges
5
enter edges
10
02
21
03
34
Bridges in graph
34
03
A vertex is said to be an articulation point in a graph if removal of the vertex and associated
edges disconnects the graph. So, the removal of articulation points increases the number of
connected components in a graph.
Articulation points are sometimes called cut vertices. The main aim here is to find out all the
articulations points in a graph.
Example: Given an undirected graph G, then find all the articulation points in the graph.
Output = 0,3
Algorithm:
This is a DFS based algorithm to find all the articulation points in a graph. Given a graph,
the algorithm first constructs a DFS tree.
Initially, the algorithm chooses any random vertex to start the algorithm and marks its status
as visited. The next step is to calculate the depth of the selected vertex. The depth of each
vertex is the order in which they are visited.
Next, we need to calculate the lowest discovery number. This is equal to the depth of the
vertex reachable from any vertex by considering one back edge in the DFS tree. An edge is a
back edge if is an ancestor of edge but not part of the DFS tree. But the edge is a part of the
original graph.
After calculating the depth and lowest discovery number for the first picked vertex, the
algorithm then searches for its adjacent vertices. It checks whether the adjacent vertices
are already visited or not. If not, then the algorithm marks it as the current vertex and
calculates its depth and lowest discovery number.
import java.util.*;
class ArticulationPoint
{
static int time;
static void APUtil(ArrayList<ArrayList<Integer> > adj, int u,boolean visited[], int disc[], int
low[],int parent, boolean isAP[])
{
// Count of children in DFS Tree
int children = 0;
// Check if the subtree rooted with v has a connection to one of the ancestors of u
low[u] = Math.min(low[u], low[v]);
// If u is not root and low value of one of its child is more than discovery value of u.
if (parent != -1 && low[v] >= disc[u])
isAP[u] = true;
}
// Adding this loop so that the code works even if we are given disconnected graph
for (int u = 0; u < V; u++)
if (visited[u] == false)
APUtil(adj, u, visited, disc, low, par, isAP);
g.addEdge(adj1,end1,end2);
}
One common approach to solving the max flow problem is the Ford-Fulkerson algorithm,
which is based on the idea of augmenting paths. The algorithm starts with an initial flow of zero,
and iteratively finds a path from s to t that has available capacity, and then increases the flow
along that path by the maximum amount possible. This process continues until no more
augmenting paths can be found.
Another popular algorithm for solving the max flow problem is the Edmonds-Karp algorithm,
which is a variant of the Ford-Fulkerson algorithm that uses breadth-first search to find
augmenting paths, and thus can be more efficient in some cases.
Ford-Fulkerson method:
Ford-Fulkerson Example:
Step 1: Select any arbitrary path from S to T. In this step,we have selected path S-A-B-T
The minimum capacity among the three edges is 2 (B-T) Based on this, update the flow/capacity for
each path.
Step2:Select another path S-D-C-T. The minimum capacity among these edges is 3(S-D)
Step 3: Now, let us consider the reverse-path B-D as well. Selecting path S-A-B-D-C-T. The
minimum residual capacity among the edges is 1 (D-C).
Step 4: Adding all the flows = 2 + 3 + 1 = 6, which is the maximum possible flow on the flow
network.
Example 2:
Input: 6
0 16 13 0 0 0
0 0 10 12 0 0
0 4 0 0 14 0
0 0 9 0 0 20
000704
000000
05
Output: 23
import java.util.*;
public class MaxFlow
{
static int V; // number of vertices in the graph
// method to find the maximum flow in a flow network using the Edmonds-Karp algorithm
static int findMaxFlow(int[][] graph, int source, int sink)
{
int[][] residualGraph = new int[V][V];
for (int i = 0; i < V; i++)
{
for (int j = 0; j < V; j++)
{
residualGraph[i][j] = graph[i][j];
}
}
int[] parent = new int[V];
int maxFlow = 0;
while (!queue.isEmpty())
{
int u = queue.poll();
for (int v = 0; v < V; v++)
{
Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.
According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined
between two nodes ‘p’ and ‘q’ as the lowest node in ‘T’ that has both ‘p’ and ‘q’ as descendants
(where we allow a node to be a descendant of itself).”
Example-1:-
Output: 3
Explanation: The LCA of nodes 5 and 1 is 3, since a node can be a descendant of itself according to
the LCA definition.
Example-2:-
Example 3:
Input: root = [1,2], p = 1, q = 2
Output: 1
Approach:
ii)Right subtree
If the left subtree recursive call gives a null value that means we haven’t found LCA in the
left subtree, which means we found LCA on the right subtree. So we will return right.
If the right subtree recursive call gives null value, that means we haven’t found LCA on the
right subtree, which means we found LCA on the left subtree. So we will return left .
If both left & right calls give values (not null) that means the root is the LCA.
Let’s take an example and will try to understand the approach more clearly:
Example:
Input: x = 4, y = 5
Output: 2
Root is 1 which is not null and x,y is not equal to root, So the 1st statement in approach will not
execute.
i) Call left subtree, While calling recursively it will find 4 and this call will return 4 to its
parent
Point to Note: At present, the root is 2 (Look at below recursion tree for better understanding)
i) Call the right subtree ( i.e right of 2), While calling recursively it will find 5 and this call
will return 5 to its parent.
Now the left recursive call returns value (not null) i.e 4 and also the right recursive call
returns value (not null) i.e 5 to its root ( at present root is 2) , and this 2 will return itself to its
root i.e to 1 (main root).
Point to Note: At present, the root is 1 (Look at below recursion tree for better understanding)
class LowestCommonAncestor
{
static int depth[],parent[];
static boolean visited[];
static BinaryTreeNode root;
//static BinaryTreeNode temp = root;
static void insert(BinaryTreeNode temp, int key)
{
if (temp == null)
{
root = new BinaryTreeNode(key);
return;
}
Queue<BinaryTreeNode> q = new LinkedList<BinaryTreeNode>();
q.add(temp);
}
else
q.add(temp.left);
if (temp.right == null)
{
temp.right = new BinaryTreeNode(key);
break;
}
else
q.add(temp.right);
}
}
public static void dfs(BinaryTreeNode root)
{
visited[root.data]=true;
if(root.left!=null)
{
depth[root.left.data]=depth[root.data]+1;
parent[root.left.data]=root.data;
dfs(root.left);
}
if(root.right!=null)
{
depth[root.right.data]=depth[root.data]+1;
parent[root.right.data]=root.data;
dfs(root.right);
}
}
{
depth[v.data]=depth[parent[v.data]];
v.data=parent[v.data];
}
}
while(u.data!=v.data)
{
u.data=parent[u.data];
v.data=parent[v.data];
}
return u;
}
public static void main(String args[])
{
Scanner sc=new Scanner(System.in);
String str[]=sc.nextLine().split(" ");
root=new BinaryTreeNode(Integer.parseInt(str[0]));
for(int i=1 ; i<str.length; i++)
insert(root,Integer.parseInt(str[i]));
BinaryTreeNode p=new BinaryTreeNode(sc.nextInt());
BinaryTreeNode q=new BinaryTreeNode(sc.nextInt());
depth=new int[100];
parent=new int[100];
visited=new boolean[100];
depth[root.data]=1;
dfs(root);
BinaryTreeNode res=lca(root,p,q);
System.out.println(res.data);
}
}
Input=1 2 3 4 5 6 7 8 9 10 11
78
Output=1
Input=11 99 88 77 22 33 66 55 10 20 30 40 50 60 44
66 55
Output=11
Topological Sort:
Applications:
1. Parallel Courses.
2. Course Schedule.
Introduction:
Topological Sorting or Kahn's algorithm is an algorithm that orders a directed acylic graph in
a way such that each node appears before all the nodes it points to in the returned order, i.e. if
we have a --> b, a must appear before b in the topological order.
It's main usage is to detect cycles in directed graphs, since no topological order is possible for
a graph that contains a cycle. Some of it's uses are: deadlock detection in OS, Course
schedule problem etc.
The topological sort algorithm takes a directed graph and returns an array of the nodes
where each node appears before all the nodes it points to.
The ordering of the nodes in the array is called a topological ordering.
Example:
Since node 1 points to nodes 2 and 3, node 1 appears before them in the ordering. And, since
nodes 2 and 3 both point to node 4, they appear before it in the ordering.
Example:
1. Parallel Courses:
1.1.Parallel Courses-I:
You are given an integer n, which indicates that there are n courses labeled from 1 to n. You are
also given an array relations where relations[i] = [prevCoursei, nextCoursei], representing a
prerequisite relationship between course prevCoursei and course nextCoursei: course
prevCoursei has to be taken before course nextCoursei.
In one semester, you can take any number of courses as long as you have taken all the
prerequisites in the previous semester for the courses you are taking.
Return the minimum number of semesters needed to take all courses. If there is no way to take
all the courses, return -1.
Example 1:
Example 2:
Solution:
We can first build a graph g𝑔 to represent the prerequisite relationships between courses, and count
the in-degree 𝑖𝑛𝑑𝑒𝑔 of each course.
Then we enqueue the courses with an in-degree of 0 and start topological sorting. Each time, we
dequeue a course from the queue, reduce the in-degree of the courses that it points to by 1, and if the
in-degree becomes 0 after reduction, we enqueue that course. When the queue is empty, if there are
still courses that have not been completed, it means that it is impossible to complete all courses, so
we return −1. Otherwise, we return the number of semesters required to complete all courses.
The time complexity is 𝑂(𝑛+𝑚), and the space complexity is 𝑂(𝑛+𝑚). Here, 𝑛 and 𝑚 are the number
of courses and the number of prerequisite relationships, respectively.
class ParallelCourses_I
{
public int minimumSemesters(int n, int[][] relations)
{
List<Integer>[] g = new List[n];
Arrays.setAll(g, k -> new ArrayList<>());
int[] indeg = new int[n];
for (var r : relations) {
int prev = r[0] - 1, nxt = r[1] - 1;
g[prev].add(nxt);
++indeg[nxt];
}
Deque<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n; ++i)
{
if (indeg[i] == 0)
{
q.offer(i);
}
}
int ans = 0;
while (!q.isEmpty())
{
++ans;
for (int k = q.size(); k > 0; --k)
{
int i = q.poll();
--n;
for (int j : g[i])
{
if (--indeg[j] == 0)
{
q.offer(j);
}
}
}
}
return n == 0 ? ans : -1;
}
}
1.2.Parallel Courses-II:
You are given an integer n, which indicates that there are n courses labeled from 1 to n. You are
also given an array relations where relations[i] = [prevCoursei, nextCoursei], representing a
prerequisite relationship between course prevCoursei and course nextCoursei:
course prevCoursei has to be taken before course nextCoursei. Also, you are given the integer k.
In one semester, you can take at most k courses as long as you have taken all the prerequisites
in the previous semesters for the courses you are taking.
Return the minimum number of semesters needed to take all courses. The testcases will be
generated such that it is possible to take every course.
Example 1:
Example 2:
import java.util.*;
public class ParallelCourses_II
{
public int minimumSemesters(int numCourses, int[][] prerequisites,int maxCourses)
{
// create an adjacency list to represent the graph
int graph[][]=new int[numCourses][numCourses];
int[] indegree = new int[numCourses];
{
if(graph[u][v]==1&&--indegree[v] == 0)
queue.offer(v);
}
}
semesters++;
}
if (coursesTaken != numCourses)
{
return -1; // cannot complete all courses
}
return semesters;
}
public static void main(String[] args)
{ Scanner s=new Scanner(System.in);
int numCourses=s.nextInt();
int c=s.nextInt();
int prerequisites[][]=new int[c][2];
for(int i=0;i<c;i++)
{
for(int j=0;j<2;j++)
{
prerequisites[i][j]=s.nextInt();
}
}
int maxCourses=s.nextInt();
ParallelCourses_II p=new ParallelCourses_II ();
System.out.println(p.minimumSemesters(numCourses,prerequisites,maxCourses));
}
}
Input: 4
21
31
14
2
Output: 3
2. Course Schedule:
There are n courses — course 0 to n-1. Some of them have prerequisites. For example,
courses A and B must be completed before course C can be taken (in other words, course C
depends on A and B).
Find and return an order in which all the given courses can be taken, while satisfying all the
prerequisites. If there exists more than one such order, any one of them would be a correct
answer. If no such order exists, return a special value: [-1].
Example:
Input: n=4, prerequisites=[[1, 0], [2, 0], [3, 1], [3, 2]]
Output: [0, 1, 2, 3]
There are two orders in which one can take all four courses: [0, 1, 2, 3] and
Notes:
Function accepts two arguments: The number of courses n and the list of
prerequisites.
Prerequisites are given in the form of a two-dimensional array (or a list of lists) of
integers. Each inner array has exactly two elements — it is essentially a list of pairs.
Each pair [X, Y] represents one prerequisite: Course Y must be completed before X
X depends on Y).
Function must return an array (list) of integers.
If all given courses can be taken while satisfying all given prerequisites, the returned
array must contain a possible ordering (if more than one such ordering exists, any one
must be returned). Otherwise, the function must return an array (list) with one
element -1 in it.
Approach:
This is a Topological Sort problem. We’ll cover two efficient sample solutions: one uses
DFS, while the second keeps track of the in-degree of the graph nodes. They have the same
time and space complexity in the Big-O notation in terms of the number of courses and the
number of prerequisites.
Both algorithms need a directed graph to work with. So, let us build one.
For example, if n=4 and prerequisites= [[1, 0], [2, 0], [3, 1], [3, 2]], the graph would look like this:
The correct answer to the problem will be a topological order of the nodes. The two sample
solutions discussed below are essentially two different implementations of the topological
sort algorithm.
Topological ordering doesn’t exist if the graph has a cycle. Both sample solutions detect
cycles in their respective ways and return the special value in that case.
import java.util.*;
Case=2
Input=4
4
10
20
31
32
Output =0 1 2 3 true