Graph¶
1. Learning¶
These problems introduce the fundamental concepts of graphs, including representation and traversal techniques.
/*Question:-
Count the number of undirected graphs that can be formed with n vertices.
Approach:-
- calculate the number of edges with formula n*n-1/2
- return pow(2,edges)
Complexity Analysis:-
Time Complexity = O(1)
Space Complexity = O(1)
*/
int countingGraphs(int n)
{
int edges = (n*(n-1))/2;
return pow(2,edges);
}
/*
Question:
Given the adjacency matrix representation of an undirected graph, your task is to return the adjacency list of the graph where each adjacency list contains the vertex itself at the first position and then all its adjacent nodes.
Explanation:
- We initialize an empty adjacency list vector of vectors.
- For each vertex in the graph, we add the vertex itself as the first element of its adjacency list.
- Then, for each edge (u, v) in the graph, we add v to the adjacency list of u and u to the adjacency list of v.
- Finally, we return the adjacency list.
Time Complexity:
- The time complexity is O(m), where m is the number of edges in the graph, as we iterate through each edge once.
Space Complexity:
- The space complexity is O(n + 2 * m), where n is the number of vertices and 2 * m is the total number of elements in all adjacency lists, as each edge is represented twice (once in the adjacency list of u and once in the adjacency list of v).
*/
vector<vector<int>> printAdjacency(int n, int m, vector<vector<int>>& edges) {
vector<vector<int>> adj(n);
for (int i = 0; i < n; i++) {
adj[i].push_back(i);
}
for (auto e : edges) {
adj[e[0]].push_back(e[1]);
adj[e[1]].push_back(e[0]);
}
return adj;
}
/*
Question:
Given a directed graph, perform Breadth First Traversal (BFS) of the graph starting from vertex 0 and visit all the nodes directly or indirectly connected to Node 0.
Explanation:
- We initialize an empty vector 'ans' to store the BFS traversal.
- We also initialize a vector 'vis' to keep track of visited nodes, initialized with 'false' for all nodes.
- We use a queue 'q' to perform BFS. We start by pushing vertex 0 into the queue and mark it as visited.
- While the queue is not empty, we pop the front element and add it to the 'ans' vector.
- For each adjacent vertex of the current node, if it has not been visited, we push it into the queue and mark it as visited.
- We continue this process until the queue becomes empty and all connected nodes are visited.
- Finally, we return the 'ans' vector containing the BFS traversal.
Time Complexity:
- The time complexity is O(V + E), where V is the number of vertices and E is the number of edges in the graph. In the worst case, we visit all the vertices and edges.
Space Complexity:
- The space complexity is O(V), where V is the number of vertices, as we use extra space for the 'vis' vector and the 'q' queue.
*/
vector<int> bfsOfGraph(int V, vector<int> adj[]) {
vector<int> ans;
vector<bool> vis(V, false);
queue<int> q;
q.push(0);
vis[0] = true;
while (!q.empty()) {
int node = q.front();
q.pop();
ans.push_back(node);
for (auto i : adj[node]) {
if (!vis[i]) {
q.push(i);
vis[i] = true;
}
}
}
return ans;
}
/*
Question:
Given a connected undirected graph, perform Depth First Traversal (DFS) of the graph starting from vertex 0 and visit all the nodes directly or indirectly connected to Node 0.
Explanation:
- We initialize an empty vector 'ans' to store the DFS traversal.
- We also initialize a vector 'vis' to keep track of visited nodes, initialized with 'false' for all nodes.
- We start DFS from vertex 0 by calling the recursive function 'dfs'.
- In the 'dfs' function, we push the current node into the 'ans' vector and mark it as visited.
- For each adjacent vertex of the current node, if it has not been visited, we call the 'dfs' function recursively for that vertex.
- We continue this process until all connected nodes are visited.
- Finally, we return the 'ans' vector containing the DFS traversal.
Time Complexity:
- The time complexity is O(V + E), where V is the number of vertices and E is the number of edges in the graph. In the worst case, we visit all the vertices and edges.
Space Complexity:
- The space complexity is O(V), where V is the number of vertices, as we use extra space for the 'vis' vector.
*/
void dfs(int node, vector<int> adj[], vector<int>& ans, vector<bool>& vis){
ans.push_back(node);
vis[node] = true;
for(auto i : adj[node]){
if(!vis[i])
dfs(i, adj, ans, vis);
}
}
vector<int> dfsOfGraph(int V, vector<int> adj[]) {
vector<int> ans;
vector<bool> vis(V, false);
dfs(0, adj, ans, vis);
return ans;
}
2. Traversal Problems¶
/*
Question:
Given an n x n matrix 'isConnected' representing connections between cities (nodes), find the total number of provinces in the graph.
Explanation:
- The problem can be solved using Depth First Search (DFS).
- We first convert the given matrix 'isConnected' into an adjacency list 'adj' representing the graph.
- We use a 'vis' vector to keep track of visited cities, initialized to 'false' for all cities.
- We initialize a variable 'ans' to store the number of provinces.
- We then perform DFS from each city and mark all directly or indirectly connected cities as visited.
- We increment the 'ans' for each unvisited city and continue the process until all cities are visited.
- Finally, we return the 'ans' which represents the total number of provinces in the graph.
Time Complexity:
- The time complexity is O(n^2), where n is the number of cities. We traverse the entire 'isConnected' matrix to construct the adjacency list.
Space Complexity:
- The space complexity is O(n), where n is the number of cities. We use extra space for the adjacency list and the 'vis' vector.
*/
void dfs(int node, vector<int> adj[], vector<bool>& vis){
vis[node] = true;
for(auto v : adj[node]){
if(!vis[v])
dfs(v, adj, vis);
}
}
int findCircleNum(vector<vector<int>>& isConnected) {
int ans = 0, n = isConnected.size();
vector<int> adj[n];
// Convert isConnected matrix into adjacency list
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
if(isConnected[i][j] == 1){
adj[i].push_back(j);
}
}
}
vector<bool> vis(n, false);
// Perform DFS to find the number of provinces
for(int i=0; i<n; i++){
if(!vis[i]){
ans++;
dfs(i, adj, vis);
}
}
return ans;
}
/*
Question:
You are given an m x n grid where each cell can have one of three values:
0 representing an empty cell,
1 representing a fresh orange, or
2 representing a rotten orange.
Every minute, any fresh orange that is 4-directionally adjacent to a rotten orange becomes rotten.
Return the minimum number of minutes that must elapse until no cell has a fresh orange. If this is impossible, return -1.
Example:
Input: grid = [[2,1,1],[1,1,0],[0,1,1]]
Output: 4
Approach:
1. Use Breadth-First Search (BFS) to rot the oranges.
2. Initialize a queue to store the rotten oranges.
3. Iterate through the grid to find the rotten oranges and count the number of fresh oranges.
4. Perform BFS starting from each rotten orange and keep track of the time required to rot all the fresh oranges.
5. For each level in BFS (each minute), check adjacent cells to a rotten orange and mark them as rotten if they contain a fresh orange. Decrease the fresh count and add the newly rotten oranges to the queue.
6. Repeat this process until all fresh oranges are rotten or the queue is empty.
7. Return the time required (minutes) if all fresh oranges are rotten, otherwise, return -1.
Time Complexity: O(m * n) - where 'm' is the number of rows and 'n' is the number of columns in the grid.
Space Complexity: O(m * n) - due to the queue and the grid.
Code:
*/
int orangesRotting(vector<vector<int>>& grid) {
int ans = -1, fresh = 0, m = grid.size(), n = grid[0].size();
queue<pair<int, int>> q;
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(grid[i][j] == 2) {
q.push({i, j});
}
if(grid[i][j] == 1) {
fresh++;
}
}
}
if(fresh == 0) {
return 0;
}
vector<int> dr = {-1, 1, 0, 0};
vector<int> dc = {0, 0, -1, 1};
while(!q.empty()) {
int k = q.size();
while(k--) {
auto p = q.front();
q.pop();
int x = p.first, y = p.second;
for(int i = 0; i < 4; i++) {
int nx = x + dr[i], ny = y + dc[i];
if(nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == 1) {
grid[nx][ny] = 2;
q.push({nx, ny});
fresh--;
}
}
}
ans++;
}
return (fresh > 0) ? -1 : ans;
}
/*
QUESTION:
An image is represented by an m x n integer grid image where image[i][j] represents the pixel value of the image.
You are also given three integers sr, sc, and color. You should perform a flood fill on the image starting from the pixel image[sr][sc].
To perform a flood fill, consider the starting pixel, plus any pixels connected 4-directionally to the starting pixel of the same color as the starting pixel, plus any pixels connected 4-directionally to those pixels (also with the same color), and so on. Replace the color of all of the aforementioned pixels with color.
Return the modified image after performing the flood fill.
Example:
Input: image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2
Output: [[2,2,2],[2,2,0],[2,0,1]]
Explanation: From the center of the image with position (sr, sc) = (1, 1) (i.e., the red pixel), all pixels connected by a path of the same color as the starting pixel (i.e., the blue pixels) are colored with the new color.
Note the bottom corner is not colored 2, because it is not 4-directionally connected to the starting pixel.
CODE:
*/
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
vector<vector<int>> ans(image.begin(), image.end());
int m = image.size(), n = image[0].size();
queue<pair<int, int>> q;
q.push({sr, sc});
int s = ans[sr][sc];
ans[sr][sc] = color;
vector<int> dr = {-1, 1, 0, 0};
vector<int> dc = {0, 0, -1, 1};
while (!q.empty()) {
int siz = q.size();
while (siz--) {
auto p = q.front();
q.pop();
int x = p.first, y = p.second;
for (int i = 0; i < 4; i++) {
int nx = x + dr[i], ny = y + dc[i];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && ans[nx][ny] == s && ans[nx][ny] != color) {
ans[nx][ny] = color;
q.push({nx, ny});
}
}
}
}
return ans;
}
/*
QUESTION:-
Given an undirected graph with V vertices and E edges, check whether it contains any cycle or not. Graph is in the form of adjacency list where adj[i] contains all the nodes ith node is having an edge with.
APPROACH:
- To check whether the graph contains a cycle or not, we can perform a Depth-First Search (DFS) traversal on the graph and keep track of the visited nodes.
- During the DFS traversal, if we encounter a node that is already visited and is not the parent of the current node (indicating a back edge), then there is a cycle in the graph. We need to check this condition for every node in the graph.
COMPLEXITY ANALYSIS:
- Time Complexity: O(V + E), where V is the number of vertices and E is the number of edges in the graph. In the worst case, we may need to visit all the vertices and edges of the graph.
- Space Complexity: O(V), where V is the number of vertices. We use an additional array to keep track of visited nodes.
*/
bool dfs(int node, int parent, vector<int> adj[], vector<bool>& vis){
vis[node] = true;
for(auto v:adj[node]){
if(!vis[v]){
if(dfs(v,node,adj,vis))
return true;
}
else if(v!=parent){
return true;
}
}
return false;
}
bool isCycle(int V, vector<int> adj[]) {
vector<bool> vis(V);
for(int i=0; i<V; i++){
if(!vis[i]){
if(dfs(i,-1,adj,vis))
return true;
}
}
return false;
}
/*
QUESTION:-
Given an m x n binary matrix mat, return the distance of the nearest 0 for each cell.
The distance between two adjacent cells is 1.
APPROACH:
- We can use a Breadth-First Search (BFS) traversal to find the distance of the nearest 0 for each cell.
- First, we initialize the distance matrix with -1 for all cells.
- Then, we iterate through the matrix and find all cells with the value 0. For each 0 cell found, we add it to the queue and set its distance to 0 in the distance matrix.
- Next, we perform a BFS starting from the cells with 0. During the BFS, we update the distance of each cell from the nearest 0 cell and continue the BFS until all cells are visited.
- Finally, we return the distance matrix.
COMPLEXITY ANALYSIS:
- Time Complexity: O(m * n), where m is the number of rows and n is the number of columns in the matrix. In the worst case, we may need to visit all the cells of the matrix.
- Space Complexity: O(m * n), where m is the number of rows and n is the number of columns in the matrix. We use additional space for the distance matrix and the queue during BFS.
*/
vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
int m = mat.size();
int n = mat[0].size();
vector<vector<int>> dis(m, vector<int>(n, -1));
queue<pair<int, int>> q;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(mat[i][j] == 0){
dis[i][j] = 0;
q.push({i, j});
}
}
}
vector<int> dr = {-1, 1, 0, 0};
vector<int> dc = {0, 0, -1, 1};
int level = 1;
while(!q.empty()){
int size = q.size();
while(size--){
auto p = q.front();
q.pop();
int x = p.first, y = p.second;
for(int i = 0; i < 4; i++){
int nx = x + dr[i], ny = y + dc[i];
if(nx >= 0 && nx < m && ny >= 0 && ny < n && dis[nx][ny] == -1){
dis[nx][ny] = level;
q.push({nx, ny});
}
}
}
level++;
}
return dis;
}
/*
QUESTION:-
Given an m x n matrix board containing 'X' and 'O', capture all regions that are 4-directionally surrounded by 'X'.
A region is captured by flipping all 'O's into 'X's in that surrounded region.
APPROACH:
- We can use Depth-First Search (DFS) to find all regions that are surrounded by 'X'.
- First, we initialize a copy of the board called 'vis' to store the visited status of each cell.
- Then, we perform a DFS starting from all border cells that have 'O's. During the DFS, we mark all connected 'O's as visited by changing them to a special character, such as '#', in the 'vis' matrix.
- After performing DFS from border cells, all remaining '#'s in the 'vis' matrix represent regions that are not surrounded by 'X'.
- Finally, we update the original board by flipping all remaining 'O's to 'X'.
COMPLEXITY ANALYSIS:
- Time Complexity: O(m * n), where m is the number of rows and n is the number of columns in the matrix. In the worst case, we may need to visit all the cells of the matrix during DFS.
- Space Complexity: O(m * n), where m is the number of rows and n is the number of columns in the matrix. We use additional space for the 'vis' matrix.
*/
void dfs(int i, int j, vector<vector<char>>& vis){
if(i >= 0 && i < vis.size() && j >= 0 && j < vis[0].size() && vis[i][j] == 'O'){
vis[i][j] = '#';
dfs(i + 1, j, vis);
dfs(i - 1, j, vis);
dfs(i, j + 1, vis);
dfs(i, j - 1, vis);
}
}
void solve(vector<vector<char>>& board) {
int m = board.size(), n = board[0].size();
vector<vector<char>> vis(board.begin(), board.end());
// Perform DFS from border cells
for(int i = 0; i < n; i++){
if(vis[0][i] == 'O')
dfs(0, i, vis);
if(vis[m - 1][i] == 'O')
dfs(m - 1, i, vis);
}
for(int i = 0; i < m; i++){
if(vis[i][0] == 'O')
dfs(i, 0, vis);
if(vis[i][n - 1] == 'O')
dfs(i, n - 1, vis);
}
// Update the original board
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(vis[i][j] == 'O')
board[i][j] = 'X';
}
}
}
/*
QUESTION:-
You are given an m x n binary matrix grid, where 0 represents a sea cell and 1 represents a land cell.
A move consists of walking from one land cell to another adjacent (4-directionally) land cell or walking off the boundary of the grid.
Return the number of land cells in grid for which we cannot walk off the boundary of the grid in any number of moves.
APPROACH:
- We can use Depth-First Search (DFS) to mark all land cells connected to the boundary of the grid as uncountable (i.e., cells that we can walk off the boundary).
- First, we initialize a copy of the grid called 'vis' to store the visited status of each cell.
- Then, we perform DFS from all land cells located at the boundary of the grid. During the DFS, we mark all connected land cells as visited by changing their value to -1 in the 'vis' matrix.
- After performing DFS from boundary cells, all remaining land cells in the 'vis' matrix represent cells that we cannot walk off the boundary of the grid.
- Finally, we count the number of land cells in the 'vis' matrix and return the count as the result.
COMPLEXITY ANALYSIS:
- Time Complexity: O(m * n), where m is the number of rows and n is the number of columns in the matrix. In the worst case, we may need to visit all the cells of the matrix during DFS.
- Space Complexity: O(m * n), where m is the number of rows and n is the number of columns in the matrix. We use additional space for the 'vis' matrix.
*/
void dfs(int i, int j, vector<vector<int>>& vis){
if(i >= 0 && i < vis.size() && j >= 0 && j < vis[0].size() && vis[i][j] == 1){
vis[i][j] = -1;
dfs(i + 1, j, vis);
dfs(i - 1, j, vis);
dfs(i, j + 1, vis);
dfs(i, j - 1, vis);
}
}
int numEnclaves(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> vis(grid.begin(), grid.end());
// Perform DFS from boundary cells
for(int i = 0; i < n; i++){
if(vis[0][i] == 1)
dfs(0, i, vis);
if(vis[m - 1][i] == 1)
dfs(m - 1, i, vis);
}
for(int i = 0; i < m; i++){
if(vis[i][0] == 1)
dfs(i, 0, vis);
if(vis[i][n - 1] == 1)
dfs(i, n - 1, vis);
}
// Count the number of land cells that are not on the boundary
int ans = 0;
for(auto row : vis){
for(auto col : row){
if(col == 1)
ans++;
}
}
return ans;
}
/*
QUESTION:
A transformation sequence from word beginWord to word endWord using a dictionary wordList is a sequence of words beginWord -> s1 -> s2 -> ... -> sk such that:
1. Every adjacent pair of words differs by a single letter.
2. Every si for 1 <= i <= k is in wordList. Note that beginWord does not need to be in wordList.
3. sk == endWord
Given two words, beginWord and endWord, and a dictionary wordList, return the number of words in the shortest transformation sequence from beginWord to endWord, or 0 if no such sequence exists.
APPROACH:
- We can model this problem as a graph where each word is a node and there is an edge between two words if they differ by a single letter.
- First, we create an adjacency list to represent the graph based on the given wordList. Two words are adjacent if they differ by one character.
- Then, we use Breadth-First Search (BFS) to find the shortest transformation sequence from beginWord to endWord.
- We start BFS from the beginWord and traverse the graph level by level, marking visited words to avoid revisiting them.
- During the BFS, if we reach the endWord, we return the length of the transformation sequence.
COMPLEXITY ANALYSIS:
- Time Complexity: O(n^2 * m), where n is the size of wordList and m is the average length of the words in wordList. In the worst case, we may need to compare every pair of words in wordList to create the adjacency list.
- Space Complexity: O(n^2), where n is the size of wordList. We use additional space for the adjacency list and the visited map.
*/
bool isAdj(string& a, string& b){
int cnt = 0;
for(int i = 0; i < a.size(); i++){
if(a[i] != b[i]){
if(cnt > 0) return false;
cnt++;
}
}
return true;
}
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_map<string, vector<string>> adj;
wordList.push_back(beginWord);
int n = wordList.size();
// Create the adjacency list
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
if(i != j && isAdj(wordList[i], wordList[j])){
adj[wordList[i]].push_back(wordList[j]);
}
}
}
queue<string> q;
unordered_map<string, bool> vis;
q.push(beginWord);
vis[beginWord] = true;
int lvl = 1;
// Perform BFS
while(!q.empty()){
int siz = q.size();
while(siz--){
string node = q.front();
q.pop();
if(node == endWord) return lvl;
for(auto s : adj[node]){
if(!vis[s]){
vis[s] = true;
q.push(s);
}
}
}
lvl++;
}
return 0;
}
/*
QUESTION:
Given a boolean 2D matrix grid of size n * m. You have to find the number of distinct islands where a group of connected 1s (horizontally or vertically) forms an island. Two islands are considered to be distinct if and only if one island is not equal to another (not rotated or reflected).
APPROACH:
- We can model this problem as a graph where each group of connected 1s forms an island.
- We can use Depth-First Search (DFS) to traverse the grid and identify each island.
- During the DFS, we keep track of the path taken to traverse each island. The path can be represented as a string, where each character in the string represents the direction of movement during the DFS (U for Up, D for Down, R for Right, and L for Left).
- We use a set to store the paths of all distinct islands. As sets store unique elements, we will have only unique paths in the set.
- Finally, we return the size of the set, which gives us the number of distinct islands.
COMPLEXITY ANALYSIS:
- Time Complexity: O(n * m), where n is the number of rows and m is the number of columns in the grid. We visit each cell at most once during the DFS.
- Space Complexity: O(n * m), where n is the number of rows and m is the number of columns in the grid. We use additional space to store the visited status of each cell and the paths of the islands in the set.
*/
void dfs(int i, int j, vector<vector<int>>& grid, string& path, vector<vector<bool>>& vis){
if(i >= 0 && i < grid.size() && j >= 0 && j < grid[0].size() && grid[i][j] == 1 && !vis[i][j]){
vis[i][j] = true;
dfs(i + 1, j, grid, path += "U", vis);
dfs(i - 1, j, grid, path += "D", vis);
dfs(i, j + 1, grid, path += "R", vis);
dfs(i, j - 1, grid, path += "L", vis);
}
}
int countDistinctIslands(vector<vector<int>>& grid) {
int n = grid.size(), m = grid[0].size();
vector<vector<bool>> vis(n, vector<bool>(m));
unordered_set<string> st;
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(grid[i][j] == 1 && !vis[i][j]){
string path = "";
dfs(i, j, grid, path, vis);
st.insert(path);
}
}
}
return st.size();
}
/*
QUESTION:
There is an undirected graph with n nodes, where each node is numbered between 0 and n - 1. You are given a 2D array graph, where graph[u] is an array of nodes that node u is adjacent to. More formally, for each v in graph[u], there is an undirected edge between node u and node v. The graph has the following properties:
1. There are no self-edges (graph[u] does not contain u).
2. There are no parallel edges (graph[u] does not contain duplicate values).
3. If v is in graph[u], then u is in graph[v] (the graph is undirected).
The graph may not be connected, meaning there may be two nodes u and v such that there is no path between them.
A graph is bipartite if the nodes can be partitioned into two independent sets A and B such that every edge in the graph connects a node in set A and a node in set B.
APPROACH:
- We can use Depth-First Search (DFS) to color the nodes in the graph such that we can partition them into two sets A and B.
- While performing the DFS, we use two colors: 1 and -1 to color the nodes. We start by coloring the first node with color 1.
- For each uncolored node, we perform DFS and color its neighbors with the opposite color.
- If we encounter a neighbor with the same color as the current node, the graph is not bipartite, and we return false.
- If the DFS completes without any conflicts, we return true, indicating that the graph is bipartite.
COMPLEXITY ANALYSIS:
- Time Complexity: O(V + E), where V is the number of nodes (vertices) in the graph, and E is the number of edges in the graph. We visit each node and each edge exactly once during the DFS.
- Space Complexity: O(V), where V is the number of nodes (vertices) in the graph. We use additional space to store the colors of the nodes.
*/
bool dfs(int node, int color, vector<vector<int>>& adj, vector<int>& vis){
vis[node] = color;
for(auto v : adj[node]){
if(!vis[v] && !dfs(v, -color, adj, vis)){
return false;
}
if(vis[v] == color){
return false;
}
}
return true;
}
bool isBipartite(vector<vector<int>>& graph) {
int n = graph.size();
vector<int> vis(n);
for(int i = 0; i < n; i++){
if(!vis[i] && !dfs(i, 1, graph, vis)){
return false;
}
}
return true;
}
/*
QUESTION:
Given a Directed Graph with V vertices (Numbered from 0 to V-1) and E edges, check whether it contains any cycle or not.
APPROACH:
- To check for cycles in a directed graph, we can use Depth-First Search (DFS) with backtracking.
- During the DFS, we maintain a visited array to keep track of nodes that have been visited.
- We perform DFS from each unvisited node to explore the graph and check for cycles.
- If we encounter a node that is already visited in the current DFS traversal, it means we have found a cycle, and we return true.
- If we complete the DFS for all nodes without finding any cycle, we return false.
COMPLEXITY ANALYSIS:
- Time Complexity: O(V + E), where V is the number of vertices (nodes) and E is the number of edges in the graph. We visit each node and each edge exactly once during the DFS.
- Space Complexity: O(V), where V is the number of vertices (nodes) in the graph. We use additional space to store the visited status of the nodes.
*/
bool dfs(int node, vector<int> adj[], vector<bool>& vis){
vis[node] = true;
for(auto v : adj[node]){
if(!vis[v] && dfs(v, adj, vis)){
return true;
}
else if(vis[v]){
return true;
}
}
vis[node] = false;
return false;
}
bool isCyclic(vector<vector<int>>& edges, int v, int e)
{
vector<int> adj[v];
for(auto it : edges){
adj[it[0]].push_back(it[1]);
}
vector<bool> vis(v);
for(int i = 0; i < v; i++){
if(!vis[i] && dfs(i, adj, vis)){
return true;
}
}
return false;
}
3. Topological Sort Problems¶
Topological sorting is used in Directed Acyclic Graphs (DAGs) to determine ordering constraints such as task scheduling and dependency resolution.
/*
QUESTION:
Given a DAG (directed acyclic graph), print the Topological sorting of a given graph.
APPROACH:
- Topological sorting is a linear ordering of vertices in a directed acyclic graph (DAG) such that for every directed edge u -> v, vertex u comes before v in the ordering.
- To find the topological sorting, we can use Depth-First Search (DFS) with backtracking.
- We perform DFS starting from each unvisited node, and as we finish exploring a node and backtrack, we add it to the front of the topological sorting order.
COMPLEXITY ANALYSIS:
- Time Complexity: O(V + E), where V is the number of vertices (nodes) and E is the number of edges in the graph. We visit each node and each edge exactly once during the DFS.
- Space Complexity: O(V), where V is the number of vertices (nodes) in the graph. We use additional space to store the visited status of the nodes and the topological sorting order.
*/
bool dfs(int node, vector<int> adj[], vector<bool>& vis, vector<int>& ans){
vis[node] = true;
for(auto v : adj[node]){
if(!vis[v]){
dfs(v, adj, vis, st);
}
}
ans.push_back(node);
}
vector<int> topologicalSort(vector<vector<int>>& graph, int edges, int nodes) {
vector<int> adj[nodes];
for(auto it : graph){
adj[it[0]].push_back(it[1]);
}
vector<bool> vis(nodes);
vector<int> ans;
for(int i = 0; i < nodes; i++){
if(!vis[i]){
dfs(i, adj, vis, ans);
}
}
reverse(ans.begin(),ans.end());
return ans;
}
/*
QUESTION:
Given a Directed Graph with V vertices (Numbered from 0 to V-1) and E edges, check whether it contains any cycle or not.
APPROACH:
- We can use Topological Sorting to check if a directed graph contains a cycle or not.
- If a directed graph is a DAG (Directed Acyclic Graph), it means it does not contain any cycle.
- So, to check for a cycle, we perform a variation of Topological Sorting using Kahn's algorithm, which is based on the concept of indegree.
- If there is no cycle in the graph, we can always find a node with an indegree of 0 (no incoming edges) and remove it along with its outgoing edges.
- We keep repeating this process, and if at any point we are unable to find a node with an indegree of 0, it means there is a cycle in the graph.
COMPLEXITY ANALYSIS:
- Time Complexity: O(V + E), where V is the number of vertices (nodes) and E is the number of edges in the graph. We perform a BFS-like traversal of all nodes and edges.
- Space Complexity: O(V), where V is the number of vertices (nodes) in the graph. We use additional space to store the indegree of each node and the queue for BFS.
*/
bool isCyclic(int V, vector<int> adj[]) {
vector<int> indeg(V, 0);
// Calculate the indegree of each node
for (int i = 0; i < V; i++) {
for (auto it : adj[i]) {
indeg[it]++;
}
}
queue<int> q;
// Find nodes with indegree 0 and add them to the queue
for (int i = 0; i < V; i++) {
if (indeg[i] == 0) {
q.push(i);
}
}
int cnt = 0;
while (!q.empty()) {
int node = q.front();
q.pop();
cnt++;
// Remove the node and its outgoing edges
for (auto i : adj[node]) {
indeg[i]--;
if (indeg[i] == 0) {
q.push(i);
}
}
}
// If the count of removed nodes is not equal to the total number of nodes, there is a cycle
return !(cnt == V);
}
/*
QUESTION:
There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.
Return true if you can finish all courses. Otherwise, return false.
APPROACH:
- We can model the problem as a directed graph, where each course is a node, and a prerequisite pair [ai, bi] indicates a directed edge from course bi to course ai.
- To check if we can finish all courses, we need to ensure that the directed graph is a DAG (Directed Acyclic Graph) with no cycles.
- If there are cycles in the graph, it means there is a circular dependency between courses, and we cannot finish all courses in such cases.
- To check for cycles, we can use Kahn's algorithm for Topological Sorting.
- If we can perform a successful Topological Sort (i.e., there is no cycle), then it means we can finish all courses.
COMPLEXITY ANALYSIS:
- Time Complexity: O(N + E), where N is the number of courses (nodes) and E is the number of prerequisites (edges) in the graph. We perform a BFS-like traversal of all nodes and edges.
- Space Complexity: O(N + E), where N is the number of courses (nodes) and E is the number of prerequisites (edges) in the graph. We use additional space to store the adjacency list and indegree of each node.
*/
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> indeg(numCourses, 0);
vector<vector<int>> adj(numCourses);
// Calculate the indegree of each course and build the adjacency list
for (auto it : prerequisites) {
indeg[it[0]]++;
adj[it[1]].push_back(it[0]);
}
queue<int> q;
// Find courses with indegree 0 and add them to the queue
for (int i = 0; i < numCourses; i++) {
if (indeg[i] == 0) {
q.push(i);
}
}
int cnt = 0;
while (!q.empty()) {
int node = q.front();
q.pop();
cnt++;
// Remove the course and its outgoing edges
for (auto v : adj[node]) {
indeg[v]--;
if (indeg[v] == 0) {
q.push(v);
}
}
}
// If the count of finished courses is equal to the total number of courses, return true
return cnt == numCourses;
}
/*
QUESTION:
There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.
Return the ordering of courses you should take to finish all courses. If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.
APPROACH:
- We can model the problem as a directed graph, where each course is a node, and a prerequisite pair [ai, bi] indicates a directed edge from course bi to course ai.
- To find the course ordering, we can use Kahn's algorithm for Topological Sorting.
- If we can perform a successful Topological Sort, it means we can finish all courses, and the order in which we pop the nodes from the queue will give us the correct course order.
- If there is a cycle in the graph (i.e., it is not possible to finish all courses), the algorithm will not be able to remove all nodes with indegree 0, and we will not get a valid course ordering.
COMPLEXITY ANALYSIS:
- Time Complexity: O(N + E), where N is the number of courses (nodes) and E is the number of prerequisites (edges) in the graph. We perform a BFS-like traversal of all nodes and edges.
- Space Complexity: O(N + E), where N is the number of courses (nodes) and E is the number of prerequisites (edges) in the graph. We use additional space to store the adjacency list and indegree of each node.
*/
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> indeg(numCourses, 0);
vector<vector<int>> adj(numCourses);
// Calculate the indegree of each course and build the adjacency list
for (auto it : prerequisites) {
indeg[it[0]]++;
adj[it[1]].push_back(it[0]);
}
queue<int> q;
// Find courses with indegree 0 and add them to the queue
for (int i = 0; i < numCourses; i++) {
if (indeg[i] == 0) {
q.push(i);
}
}
vector<int> courseOrder;
while (!q.empty()) {
int node = q.front();
q.pop();
courseOrder.push_back(node);
// Remove the course and its outgoing edges
for (auto v : adj[node]) {
indeg[v]--;
if (indeg[v] == 0) {
q.push(v);
}
}
}
// If the size of the course order is equal to the total number of courses, return the order
if (courseOrder.size() == numCourses) {
return courseOrder;
}
// If not all courses are covered, return an empty array
return {};
}
/*
QUESTION:
There is a directed graph of n nodes with each node labeled from 0 to n - 1. The graph is represented by a 0-indexed 2D integer array graph where graph[i] is an integer array of nodes adjacent to node i, meaning there is an edge from node i to each node in graph[i].
A node is a terminal node if there are no outgoing edges. A node is a safe node if every possible path starting from that node leads to a terminal node (or another safe node).
APPROACH:
- We are given a directed graph, and we need to find all the safe nodes.
- A node is safe if every possible path starting from that node leads to a terminal node (or another safe node).
- To solve this, we can use a reverse graph approach along with a topological sorting method.
- We create a reverse adjacency list, where revadj[i] contains all the nodes from which there is a directed edge to node i (i.e., nodes that have an edge towards node i).
- We also keep track of the outdegree of each node, which represents the number of outgoing edges from that node.
- We then start with nodes that have an outdegree of 0 (i.e., terminal nodes) and perform a BFS-like traversal.
- During the traversal, we keep reducing the outdegree of the nodes from which there is a directed edge to the current node.
- If a node's outdegree becomes 0 during the traversal, we add it to the queue for further processing.
- All the nodes that become terminal nodes during the traversal are safe nodes.
COMPLEXITY ANALYSIS:
- Time Complexity: O(N + E), where N is the number of nodes, and E is the number of edges in the graph. We perform a BFS-like traversal of all nodes and edges.
- Space Complexity: O(N + E), where N is the number of nodes, and E is the number of edges in the graph. We use additional space to store the reverse adjacency list and outdegree of each node.
*/
vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
int n = graph.size();
// Creating reverse adjacency list and outdegree array
vector<int> revadj[n];
vector<int> outdeg(n);
for (int i = 0; i < n; i++) {
outdeg[i] += graph[i].size();
for (auto j : graph[i]) {
revadj[j].push_back(i);
}
}
queue<int> q;
for (int i = 0; i < n; i++) {
if (outdeg[i] == 0) q.push(i);
}
vector<int> ans;
while (!q.empty()) {
int siz = q.size();
while (siz--) {
int node = q.front();
q.pop();
ans.push_back(node);
// Reduce outdegree of the nodes from which there is a directed edge to the current node
for (auto v : revadj[node]) {
outdeg[v]--;
if (outdeg[v] == 0) {
q.push(v);
}
}
}
}
// Sort the safe nodes in ascending order
sort(ans.begin(), ans.end());
return ans;
}
/*
QUESTION:
Given a sorted dictionary of an alien language having N words and k starting alphabets of the standard dictionary. Find the order of characters in the alien language.
APPROACH:
- We are given a sorted dictionary of an alien language.
- To find the order of characters in the alien language, we can use a directed graph approach along with topological sorting.
- We create a directed graph where each node represents a character, and there is a directed edge from node 'a' to node 'b' if character 'a' comes before character 'b' in the alien language.
- We also keep track of the indegree of each node, which represents the number of characters that come before it in the alien language.
- We iterate through the dictionary and set the directed edges and indegrees accordingly.
- We then start with the nodes having an indegree of 0 (i.e., the characters that come first in the alien language) and perform a BFS-like traversal.
- During the traversal, we keep reducing the indegree of the nodes that come after the current node.
- The characters that become terminal nodes (i.e., their indegree becomes 0) during the traversal are the characters that come last in the alien language.
- We construct the order of characters based on the BFS traversal, and that will be the correct order of characters in the alien language.
COMPLEXITY ANALYSIS:
- Time Complexity: O(N), where N is the number of words in the dictionary. We iterate through the dictionary once to set the directed edges and indegrees.
- Space Complexity: O(K), where K is the number of starting alphabets in the standard dictionary. We use additional space to store the directed graph and indegrees.
*/
void setAdj(string a, string b, vector<int> adj[], vector<int>& indeg){
int as = a.size(), bs = b.size();
int n = min(as,bs);
for(int i=0; i<n; i++){
if(a[i]!=b[i]){
adj[a[i]-'a'].push_back(b[i]-'a');
indeg[b[i]-'a']++;
break;
}
}
}
string findOrder(string dict[], int N, int K) {
vector<int> adj[K];
vector<int> indeg(K);
// Set the directed edges and indegrees
for(int i=1; i<N; i++){
setAdj(dict[i-1],dict[i],adj,indeg);
}
queue<int> q;
for(int i=0; i<K; i++){
if(indeg[i]==0){
q.push(i);
}
}
string ans = "";
while(!q.empty()){
int siz = q.size();
while(siz--){
int node = q.front();
q.pop();
ans += node+'a';
for(auto v:adj[node]){
indeg[v]--;
if(indeg[v]==0) q.push(v);
}
}
}
return ans;
}
4. Shortest Path Problems¶
These algorithms find the minimum distance between nodes in different types of graphs.
/*
QUESTION:
You are given an undirected graph with unit weight. Find the shortest path from the source vertex to all other vertices, and if it is unreachable to reach any vertex, then return -1 for that vertex.
APPROACH:
- To find the shortest path from the source vertex to all other vertices, we can use a BFS traversal of the graph.
- We create an adjacency list to represent the undirected graph.
- We use a queue to perform the BFS traversal, starting from the source vertex.
- During the BFS traversal, we keep track of the level (or distance) of each vertex from the source vertex.
- We maintain a boolean array to keep track of visited vertices so that we don't visit the same vertex again.
- If a vertex is reachable from the source vertex, its distance will be equal to the number of edges in the shortest path from the source vertex.
- After the BFS traversal is complete, we will have the distance of each vertex from the source vertex.
COMPLEXITY ANALYSIS:
- Time Complexity: O(N+M), where N is the number of vertices and M is the number of edges in the graph. We perform a BFS traversal, visiting each vertex and edge once.
- Space Complexity: O(N), where N is the number of vertices. We use additional space to store the adjacency list, visited array, and the distance array.
*/
vector<int> shortestPath(vector<vector<int>>& edges, int N, int M, int src){
vector<int> adj[N];
for(auto e:edges){
adj[e[0]].push_back(e[1]);
adj[e[1]].push_back(e[0]);
}
vector<bool> vis(N);
vector<int> dis(N, -1);
queue<int> q;
q.push(src);
vis[src] = true;
int lvl = 0;
while(!q.empty()){
int siz = q.size();
while(siz--){
int node = q.front();
q.pop();
dis[node] = lvl;
for(auto v:adj[node]){
if(!vis[v]){
q.push(v);
vis[v] = true;
}
}
}
lvl++;
}
return dis;
}
/*
QUESTION:
Given a Directed Acyclic Graph of N vertices from 0 to N-1 and a 2D Integer array (or vector) edges[][] of length M, where there is a directed edge from edges[i][0] to edges[i][1] with a distance of edges[i][2] for all i.
Find the shortest path from the source (vertex 0) to all other vertices, and if it is impossible to reach any vertex, then return -1 for that vertex.
APPROACH:
- To find the shortest path from the source vertex to all other vertices in a Directed Acyclic Graph (DAG), we can use a topological sorting based approach along with dynamic programming (DP).
- First, we perform a topological sorting of the DAG. This will give us a linear order in which the vertices should be visited such that there are no backward edges.
- After obtaining the topological order, we initialize the distance array with a large value (infinity) for all vertices, except the source vertex whose distance is set to 0.
- We then iterate through the vertices in the topological order and update the distance for each vertex by considering all outgoing edges from that vertex and the distances of its neighboring vertices.
- Since the graph is a DAG, we are guaranteed that we visit all the parent nodes of a vertex before visiting the vertex itself. Hence, we can safely update the distance of each vertex in a topological order.
- After updating the distance array, if any vertex remains at the initial large value (infinity), it means that it is not reachable from the source vertex, and we set its distance to -1.
COMPLEXITY ANALYSIS:
- Time Complexity: O(N + M), where N is the number of vertices and M is the number of edges in the graph. The time complexity is dominated by the topological sorting and DP updates.
- Space Complexity: O(N + M), where N is the number of vertices and M is the number of edges in the graph. We use additional space for the adjacency list, visited array, topological order, and the distance array.
*/
bool dfs(int node, vector<pair<int, int>> adj[], vector<bool>& vis, vector<int>& topo){
vis[node] = true;
for(auto v : adj[node]){
if(!vis[v.first]){
dfs(v.first, adj, vis, topo);
}
}
topo.push_back(node);
}
vector<int> shortestPath(int N, int M, vector<vector<int>>& edges){
vector<pair<int, int>> adj[N];
for(auto e : edges){
adj[e[0]].push_back({e[1], e[2]});
}
vector<bool> vis(N);
vector<int> topo;
for(int i = 0; i < N; i++){
if(!vis[i]){
dfs(i, adj, vis, topo);
}
}
reverse(topo.begin(), topo.end());
vector<int> dis(N, 1e9);
dis[0] = 0;
for(auto u : topo){
for(auto p : adj[u]){
int v = p.first, d = p.second;
dis[v] = min(dis[v], dis[u] + d);
}
}
for(int i = 0; i < N; i++){
if(dis[i] == 1e9){
dis[i] = -1;
}
}
return dis;
}
/*
QUESTION:
Given a weighted, undirected, and connected graph of V vertices and an adjacency list 'adj', where 'adj[i]' is a list of lists containing two integers where the first integer of each list j denotes there is an edge between node i and node j, and the second integer corresponds to the weight of that edge. You are also given the source vertex S. You need to find the shortest distance of all vertices from the source vertex S. You have to return a list of integers denoting the shortest distance between each node and the source vertex S.
APPROACH:
- We can use Dijkstra's algorithm to find the shortest distance from the source vertex to all other vertices in a weighted graph.
- The algorithm maintains a priority queue (min-heap) to store the vertices based on their tentative distances from the source vertex. We start with the source vertex and update the distances of its neighbors, pushing them into the priority queue.
- We continue this process until all vertices are visited and their distances are finalized. The priority queue ensures that we always pick the vertex with the minimum tentative distance for processing.
- We initialize the distance array 'dis' with a large value (infinity) for all vertices, except the source vertex whose distance is set to 0.
- We push the source vertex into the priority queue along with its distance, and then perform Dijkstra's algorithm.
- After the algorithm completes, we have the shortest distances of all vertices from the source vertex in the 'dis' array.
COMPLEXITY ANALYSIS:
- Time Complexity: O(E + log(V)), where E is the number of edges and V is the number of vertices in the graph. The time complexity is dominated by the priority queue operations in Dijkstra's algorithm.
- Space Complexity: O(V + E), where V is the number of vertices and E is the number of edges in the graph. We use additional space for the adjacency list, the distance array, and the priority queue.
*/
vector<int> dijkstra(int n, vector<vector<int>> adj[], int s){
vector<int> dis(n, 1e9);
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
dis[s] = 0;
pq.push({0, s});
while(!pq.empty()){
int u = pq.top().second;
pq.pop();
for(auto p : adj[u]){
int v = p[0], wt = p[1];
if(dis[u] + wt < dis[v]){
dis[v] = dis[u] + wt;
pq.push({dis[v], v});
}
}
}
return dis;
}
/* QUESTION:
Given an n x n binary matrix grid, return the length of the shortest clear path in the matrix. If there is no clear path, return -1.
A clear path in a binary matrix is a path from the top-left cell (i.e., (0, 0)) to the bottom-right cell (i.e., (n - 1, n - 1)) such that:
1. All the visited cells of the path are 0.
2. All the adjacent cells of the path are 8-directionally connected (i.e., they are different and they share an edge or a corner).
3. The length of a clear path is the number of visited cells of this path.
Example:
Input: grid = [[0,1],[1,0]]
Output: 2
APPROACH:
- We can use Breadth-First Search (BFS) to find the shortest path.
- Start BFS from the top-left cell (0, 0) and explore its neighboring cells in 8 directions.
- Mark the visited cells and continue BFS until reaching the bottom-right cell (n - 1, n - 1).
- The number of levels traversed in BFS will represent the length of the shortest clear path.
- If the bottom-right cell cannot be reached or there is no clear path, return -1.
COMPLEXITY ANALYSIS:
- The time complexity of BFS is O(n^2) as it explores all cells in the n x n matrix.
- The space complexity is also O(n^2) due to the usage of the queue and the visited matrix.
CODE:
*/
bool isValid(int x, int y, int n, vector<vector<bool>>& vis, vector<vector<int>>& grid) {
return (x >= 0 && x < n && y >= 0 && y < n && !vis[x][y] && grid[x][y] == 0);
}
int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
int n = grid.size();
if (grid[0][0] != 0 || grid[n - 1][n - 1] != 0) return -1;
vector<vector<bool>> vis(n, vector<bool>(n));
queue<pair<int, int>> q;
q.push({0, 0});
vis[0][0] = true;
int lvl = 1;
while (!q.empty()) {
int siz = q.size();
while (siz--) {
auto p = q.front();
q.pop();
int x = p.first, y = p.second;
if (x == n - 1 && y == n - 1) return lvl;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
int nx = x + i, ny = y + j;
if (isValid(nx, ny, n, vis, grid)) {
q.push({nx, ny});
vis[nx][ny] = true;
}
}
}
}
lvl++;
}
return -1;
}
/*QUESTION:
You are a hiker preparing for an upcoming hike. You are given heights, a 2D array of size rows x columns, where heights[row][col] represents the height of cell (row, col). You are situated in the top-left cell, (0, 0), and you hope to travel to the bottom-right cell, (rows-1, columns-1) (i.e., 0-indexed). You can move up, down, left, or right, and you wish to find a route that requires the minimum effort.
A route's effort is the maximum absolute difference in heights between two consecutive cells of the route.
Return the minimum effort required to travel from the top-left cell to the bottom-right cell.
Example:
Input: heights = [[1,2,2],[3,8,2],[5,3,5]]
Output: 2
Explanation: The route of [1,3,5,3,5] has a maximum absolute difference of 2 in consecutive cells.
This is better than the route of [1,2,2,2,5], where the maximum absolute difference is 3.
APPROACH:
- We can use Dijkstra's algorithm to find the minimum effort path from the top-left cell to the bottom-right cell.
- We will maintain a priority queue to keep track of cells with the minimum effort needed.
- Start from the top-left cell with effort 0 and explore neighboring cells in all four directions.
- Calculate the effort required to move to a neighboring cell and update the effort if it is lower than the current value.
- Continue this process until we reach the bottom-right cell.
COMPLEXITY ANALYSIS:
- Since we are using Dijkstra's algorithm, the time complexity is O((N*M) * log(N*M)), where N is the number of rows and M is the number of columns in the 2D array.
- The space complexity is O(N*M) to store the distance values.
CODE:
*/
typedef pair<int, pair<int, int>> tri;
int minimumEffortPath(vector<vector<int>>& heights) {
int n = heights.size(), m = heights[0].size();
vector<vector<int>> dis(n, vector<int>(m, 1e9));
priority_queue<tri, vector<tri>, greater<tri>> pq;
dis[0][0] = 0;
pq.push({0, {0, 0}});
vector<int> dr = {0, 0, 1, -1};
vector<int> dc = {1, -1, 0, 0};
int ans = INT_MIN;
while (!pq.empty()) {
tri t = pq.top();
pq.pop();
int x = t.second.first, y = t.second.second;
ans = max(ans, t.first);
if (x == n - 1 && y == m - 1) return ans;
for (int i = 0; i < 4; i++) {
int nx = x + dr[i], ny = y + dc[i];
if (nx >= 0 && nx < n && ny >= 0 && ny < m && (dis[nx][ny] > abs(heights[x][y] - heights[nx][ny]))) {
dis[nx][ny] = abs(heights[x][y] - heights[nx][ny]);
pq.push({dis[nx][ny], {nx, ny}});
}
}
}
return -1; // If the bottom-right cell cannot be reached
}
/* QUESTION:
There are n cities connected by some number of flights. You are given an array flights where flights[i] = [fromi, toi, pricei] indicates that there is a flight from city fromi to city toi with cost pricei.
You are also given three integers src, dst, and k, return the cheapest price from src to dst with at most k stops. If there is no such route, return -1.
Example:
Input: n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1
Output: 700
Explanation:
The graph is shown above.
The optimal path with at most 1 stop from city 0 to 3 is marked in red and has cost 100 + 600 = 700.
Note that the path through cities [0,1,2,3] is cheaper but is invalid because it uses 2 stops.
APPROACH:
- We can use Breadth-First Search (BFS) to find the cheapest price from the source to the destination with at most k stops.
- We will create an adjacency list to represent the graph, where each node will have a list of neighboring nodes with their corresponding prices.
- We will use a queue to perform BFS starting from the source node.
- During BFS, we will keep track of the distance to each node and the number of stops made so far.
- If we reach the destination node and the number of stops is less than or equal to k, we will update the minimum cost to reach the destination.
- Continue BFS until we have explored all possible paths with at most k stops.
- Return the minimum cost to reach the destination if it is reachable, otherwise return -1.
COMPLEXITY ANALYSIS:
- The time complexity is O(n*k) since we can have at most k stops and for each stop, we will explore all n nodes.
- The space complexity is O(n) to store the distance values.
CODE:
*/
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
vector<vector<pair<int, int>>> adj(n);
for (auto flight : flights) {
adj[flight[0]].push_back({flight[1], flight[2]});
}
queue<pair<int, int>> q;
q.push({src, 0});
vector<int> dis(n, 1e9);
int stops = 0;
while (!q.empty() && stops <= k) {
int size = q.size();
while (size--) {
auto [u, uwt] = q.front();
q.pop();
for (auto& [v, vwt] : adj[u]) {
if (vwt + uwt < dis[v]) {
dis[v] = uwt + vwt;
q.push({v, dis[v]});
}
}
}
stops++;
}
if (dis[dst] == 1e9)
return -1;
return dis[dst];
}
/* QUESTION:
You are given a network of n nodes, labeled from 1 to n. You are also given times, a list of travel times as directed edges times[i] = (ui, vi, wi), where ui is the source node, vi is the target node, and wi is the time it takes for a signal to travel from source to target.
We will send a signal from a given node k. Return the minimum time it takes for all the n nodes to receive the signal. If it is impossible for all the n nodes to receive the signal, return -1.
Example:
Input: times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
Output: 2
APPROACH:
- We can use Dijkstra's algorithm to find the minimum time it takes for all the nodes to receive the signal.
- We will create an adjacency list to represent the graph, where each node will have a list of neighboring nodes with their corresponding signal travel time.
- We will use a priority queue to perform Dijkstra's algorithm starting from the given node k.
- During Dijkstra's algorithm, we will keep track of the minimum time to reach each node from the given node k.
- Continue the algorithm until we have explored all nodes in the graph.
- Return the maximum time among all the nodes' signal travel times, as this represents the minimum time it takes for all the nodes to receive the signal.
COMPLEXITY ANALYSIS:
- The time complexity of Dijkstra's algorithm is O(E*log(V)), where E is the number of edges and V is the number of vertices.
- The space complexity is O(V) to store the distance values.
CODE:
*/
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<pair<int, int>> adj[n + 1];
for (auto time : times) {
adj[time[0]].push_back({time[1], time[2]});
}
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
vector<int> dis(n + 1, 1e9);
dis[k] = 0;
pq.push({0, k});
while (!pq.empty()) {
auto [uwt, u] = pq.top();
pq.pop();
for (auto [v, vwt] : adj[u]) {
if (dis[v] > uwt + vwt) {
dis[v] = uwt + vwt;
pq.push({dis[v], v});
}
}
}
int ans = INT_MIN;
for (int i = 1; i <= n; i++) {
if (dis[i] == 1e9)
return -1;
ans = max(ans, dis[i]);
}
return ans;
}
/* QUESTION:
Given a weighted, directed and connected graph of V vertices and E edges, Find the shortest distance of all the vertex's from the source vertex S.
Note: If the Graph contains a negative cycle then return an array consisting of only -1.
Example:
Input: E = [[0,1,5],[1,0,3],[1,2,-1],[2,0,1]], S = 2
Output: 1 6 0
Explanation:
For nodes 2 to 0, we can follow the path - 2 -> 0. This has a distance of 1.
For nodes 2 to 1, we can follow the path - 2 -> 0 -> 1, which has a distance of 1 + 5 = 6.
APPROACH:
- We can use Bellman-Ford algorithm to find the shortest distance of all the nodes from the given source vertex S.
- The Bellman-Ford algorithm can handle negative edge weights and identify negative cycles.
- We will initialize an array dis to store the shortest distance of each node from the source vertex. Initialize all distances to a very large value except for the source vertex, which will have a distance of 0.
- Run a loop for V-1 times (V is the number of vertices), and in each iteration, relax all the edges to minimize the distance.
- If there is a negative cycle, the distance will keep decreasing in the Vth iteration as well. In this case, we will return an array containing only -1.
- Otherwise, we return the dis array with the shortest distances of all nodes from the source vertex.
COMPLEXITY ANALYSIS:
- The time complexity of the Bellman-Ford algorithm is O(V*E), where V is the number of vertices and E is the number of edges.
- The space complexity is O(V) to store the distance values.
CODE:
*/
vector<int> bellman_ford(int V, vector<vector<int>>& edges, int S) {
vector<pair<int, int>> adj[V];
for (auto e : edges) {
adj[e[0]].push_back({e[1], e[2]});
}
vector<int> dis(V, 1e8);
dis[S] = 0;
for (int i = 0; i < V; i++) {
for (int u = 0; u < V; u++) {
for (auto vec : adj[u]) {
int v = vec.first, wt = vec.second;
if (dis[u] != 1e8)
dis[v] = min(dis[v], dis[u] + wt);
}
}
}
for (int u = 0; u < V; u++) {
for (auto vec : adj[u]) {
int v = vec.first, wt = vec.second;
if (dis[u] != 1e8 && dis[v] > dis[u] + wt)
return {-1};
}
}
return dis;
}
/* QUESTION:
The problem is to find the shortest distances between every pair of vertices in a given edge-weighted directed graph.
The graph is represented as an adjacency matrix of size n*n. Matrix[i][j] denotes the weight of the edge from i to j.
If Matrix[i][j] = -1, it means there is no edge from i to j.
Example:
Input: matrix = {{0, 25}, {-1, 0}}
Output: {{0, 25}, {-1, 0}}
Explanation: The shortest distance between every pair is already given (if it exists).
APPROACH:
- We will first replace all the "-1" entries in the adjacency matrix with a very large value (e.g., 1e9) to represent that there is no edge between those vertices.
- Then, we will apply the Floyd-Warshall algorithm to find the shortest distances between all pairs of vertices in the graph.
- In each iteration, we will consider an intermediate vertex k and update the shortest distance between each pair (i, j) if the path i -> k -> j is shorter than the current distance i -> j.
- After completing the algorithm, we will replace the entries with a very large value (e.g., 1e9) with -1 in the adjacency matrix to represent that there is no path between those vertices.
COMPLEXITY ANALYSIS:
- The time complexity of the Floyd-Warshall algorithm is O(V^3), where V is the number of vertices.
- The space complexity is O(1) since we are modifying the input matrix in-place.
CODE:
*/
void shortest_distance(vector<vector<int>>& matrix) {
int v = matrix.size();
for (int i = 0; i < v; i++) {
for (int j = 0; j < v; j++) {
if (matrix[i][j] == -1)
matrix[i][j] = 1e9;
}
}
for (int k = 0; k < v; k++) {
for (int i = 0; i < v; i++) {
for (int j = 0; j < v; j++) {
if (i == j)
matrix[i][j] = 0;
matrix[i][j] = min(matrix[i][j], (matrix[i][k] + matrix[k][j]));
}
}
}
for (int i = 0; i < v; i++) {
for (int j = 0; j < v; j++) {
if (matrix[i][j] == 1e9)
matrix[i][j] = -1;
}
}
}
/* QUESTION:
There are n cities numbered from 0 to n-1. Given the array edges where edges[i] = [fromi, toi, weighti] represents a bidirectional and weighted edge between cities fromi and toi, and given the integer distanceThreshold.
Return the city with the smallest number of cities that are reachable through some path and whose distance is at most distanceThreshold. If there are multiple such cities, return the city with the greatest number.
Notice that the distance of a path connecting cities i and j is equal to the sum of the edges' weights along that path.
Example:
Input: n = 4, edges = [[0, 1, 3], [1, 2, 1], [1, 3, 4], [2, 3, 1]], distanceThreshold = 4
Output: 3
Explanation: The figure above describes the graph.
The neighboring cities at a distanceThreshold = 4 for each city are:
City 0 -> [City 1, City 2]
City 1 -> [City 0, City 2, City 3]
City 2 -> [City 0, City 1, City 3]
City 3 -> [City 1, City 2]
Cities 0 and 3 have 2 neighboring cities at a distanceThreshold = 4, but we have to return city 3 since it has the greatest number.
APPROACH:
- We will use Dijkstra's algorithm to find the shortest distances from each city to all other cities in the graph.
- For each city, we will find the count of cities that are reachable through some path and whose distance is at most distanceThreshold.
- We will keep track of this count for each city in a map.
- Finally, we will find the city with the smallest count, and if there are multiple such cities, we will return the city with the greatest number.
COMPLEXITY ANALYSIS:
- The time complexity of Dijkstra's algorithm is O(V^2), where V is the number of cities.
- Since we perform Dijkstra's algorithm for each city, the overall time complexity is O(V^3).
- The space complexity is O(V) for storing the distances and the count of reachable cities for each city.
CODE:
*/
int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
vector<pair<int, int>> adj[n];
for (auto e : edges) {
adj[e[0]].push_back({e[1], e[2]});
adj[e[1]].push_back({e[0], e[2]});
}
vector<int> dis(n, numeric_limits<int>::max());
unordered_map<int, int> mp;
for (int i = 0; i < n; i++) {
dis[i] = 0;
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({0, i});
while (!pq.empty()) {
auto [uwt, u] = pq.top();
pq.pop();
for (auto [v, vwt] : adj[u]) {
if (dis[v] > uwt + vwt) {
dis[v] = uwt + vwt;
pq.push({dis[v], v});
}
}
}
int nbrCnt = 0;
for (int j = 0; j < n; j++) {
if (i != j && dis[j] <= distanceThreshold)
nbrCnt++;
dis[j] = numeric_limits<int>::max();
}
mp[i] = nbrCnt;
}
int mini = numeric_limits<int>::max(), ans = 0;
for (int i = 0; i < n; i++) {
if (mp[i] < mini) {
mini = mp[i];
ans = i;
} else if (mp[i] == mini)
ans = max(ans, i);
}
return ans;
}
/*
QUESTION:-
You are in a city that consists of n intersections numbered from 0 to n - 1 with bi-directional roads between some intersections. The inputs are generated such that you can reach any intersection from any other intersection, and that there is at most one road between any two intersections.
You are given an integer n and a 2D integer array roads where roads[i] = [ui, vi, timei] means that there is a road between intersections ui and vi that takes timei minutes to travel. You want to know in how many ways you can travel from intersection 0 to intersection n - 1 in the shortest amount of time.
Return the number of ways you can arrive at your destination in the shortest amount of time. Since the answer may be large, return it modulo 10^9 + 7.
Approach:
1. We can use Dijkstra's algorithm to find the shortest path from intersection 0 to intersection n-1 and also keep track of the number of ways to reach each intersection in the shortest time.
2. We start with intersection 0 and keep track of the minimum time required to reach each intersection.
3. During the process, we also keep track of the number of ways to reach each intersection in the shortest time.
4. If we find a shorter path to a particular intersection, we update its minimum time and reset the number of ways to reach that intersection to the number of ways to reach the previous intersection.
5. If we find an equal time path to a particular intersection, we add the number of ways to reach the previous intersection to the number of ways to reach the current intersection.
Complexity Analysis:
- Let n be the number of intersections and E be the number of roads in the input.
- Dijkstra's algorithm has a time complexity of O((n+E)log(n)) using a priority queue.
- The space complexity is O(n) to store the distance and ways arrays.
*/
#include <vector>
#include <queue>
using namespace std;
int countPaths(int n, vector<vector<int>>& roads) {
vector<pair<int, int>> adj[n];
for (auto road : roads) {
adj[road[0]].push_back({ road[1], road[2] });
adj[road[1]].push_back({ road[0], road[2] });
}
int mod = 1e9 + 7;
priority_queue<pair<long long int, long long int>, vector<pair<long long int, long long int>>, greater<pair<long long int, long long int>>> pq;
vector<long long int> dis(n, 1e18);
vector<long long int> ways(n);
dis[0] = 0;
ways[0] = 1;
pq.push({ 0, 0 });
while (!pq.empty()) {
auto [uwt, u] = pq.top();
pq.pop();
for (auto [v, vwt] : adj[u]) {
if (dis[v] > uwt + vwt) {
ways[v] = ways[u];
dis[v] = uwt + vwt;
pq.push({ dis[v], v });
}
else if (dis[v] == uwt + vwt) {
ways[v] = (ways[v] + ways[u]) % mod;
}
}
}
int ans = ways[n - 1] % mod;
return ans;
}
5. Minimum Spanning Tree (MST) Problems¶
Minimum Spanning Tree algorithms connect all vertices in a graph with minimum total edge weight.
/* QUESTION:
Given a weighted, undirected and connected graph of V vertices and E edges, the task is to find the sum of weights of the edges of the Minimum Spanning Tree.
Example:
Input:
3 3
0 1 5
1 2 3
0 2 1
Output:
4
Explanation:
The Spanning Tree resulting in a weight
of 4 is shown above.
APPROACH:
- We will use Prim's algorithm to find the Minimum Spanning Tree (MST) of the graph.
- The idea is to start from any vertex (let's say vertex 0) and add it to the MST.
- Then, we will add the edges connected to vertex 0 to a priority queue (min-heap), where the edges are sorted based on their weights in ascending order.
- We will keep adding the edges with the minimum weight to the MST and add their connected vertices to the priority queue if they are not already in the MST.
- We will repeat this process until all vertices are added to the MST.
COMPLEXITY ANALYSIS:
- The time complexity of Prim's algorithm is O(V^2) using an adjacency matrix representation, where V is the number of vertices.
- If an adjacency list representation is used, the time complexity reduces to O(E + V log V), where E is the number of edges and log V is the time complexity of priority queue operations.
- The space complexity is O(V) for the priority queue and other data structures.
CODE:
*/
int minimumSpanningTree(vector<vector<int>>& edges, int n) {
vector<pair<int, int>> adj[n];
for (auto e : edges) {
adj[e[0]].push_back({e[2], e[1]});
adj[e[1]].push_back({e[2], e[0]});
}
vector<bool> mst(n);
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({0, 0});
int ans = 0;
while (!pq.empty()) {
auto node = pq.top();
int u = node.second, uwt = node.first;
pq.pop();
if (!mst[u]) {
mst[u] = true;
ans += uwt;
for (auto vec : adj[u]) {
int v = vec.second, vwt = vec.first;
if (!mst[v])
pq.push({vwt, v});
}
}
}
return ans;
}
/* QUESTION:
Given a weighted, undirected and connected graph of V vertices and E edges, the task is to find the sum of weights of the edges of the Minimum Spanning Tree.
Example:
Input:
3 3
0 1 5
1 2 3
0 2 1
Output:
4
Explanation:
The Spanning Tree resulting in a weight
of 4 is shown above.
APPROACH:
- We will use Kruskal's algorithm to find the Minimum Spanning Tree (MST) of the graph.
- First, we will sort all the edges in non-decreasing order based on their weights.
- Then, we will initialize a Disjoint Set data structure to keep track of connected components.
- We will start adding edges one by one from the sorted list of edges to the MST.
- Before adding an edge (u, v) to the MST, we will check if u and v belong to the same connected component using the Disjoint Set data structure. If they do not belong to the same connected component, we will add the edge to the MST and union the connected components of u and v.
- We will repeat this process until all vertices are added to the MST.
COMPLEXITY ANALYSIS:
- The time complexity of Kruskal's algorithm is O(E log E), where E is the number of edges, due to the sorting step.
- The space complexity is O(V) for the Disjoint Set data structure and other data structures.
CODE:
*/
class DisjointSet {
vector<int> parent, size;
public:
DisjointSet(int n) {
parent.resize(n);
size.resize(n, 1);
for (int i = 0; i < n; i++)
parent[i] = i;
}
int findUParent(int node) {
if (parent[node] == node)
return node;
return parent[node] = findUParent(parent[node]);
}
void unionBySize(int u, int v) {
int uP = findUParent(u), vP = findUParent(v);
if (uP == vP)
return;
if (size[uP] > size[vP]) {
parent[vP] = uP;
size[uP] += size[vP];
} else {
parent[uP] = vP;
size[vP] += size[uP];
}
}
};
int minimumSpanningTree(vector<vector<int>>& edges, int n) {
DisjointSet djs(n);
sort(edges.begin(), edges.end());
int ans = 0;
for (auto e : edges) {
int wt = e[0], u = e[1], v = e[2];
if (djs.findUParent(u) != djs.findUParent(v)) {
ans += wt;
djs.unionBySize(u, v);
}
}
return ans;
}
/* QUESTION:
There are n computers numbered from 0 to n - 1 connected by ethernet cables connections forming a network where connections[i] = [ai, bi] represents a connection between computers ai and bi. Any computer can reach any other computer directly or indirectly through the network.
You are given an initial computer network connections. You can extract certain cables between two directly connected computers, and place them between any pair of disconnected computers to make them directly connected.
Return the minimum number of times you need to do this in order to make all the computers connected. If it is not possible, return -1.
APPROACH:
- We can use the Disjoint Set data structure to keep track of connected components and find the minimum number of times we need to add connections.
- If the number of connections is less than n - 1 (number of computers minus one), it means the network is disconnected, and we cannot make all the computers connected. In this case, we return -1.
- Otherwise, we initialize a Disjoint Set data structure with n nodes representing each computer.
- We iterate through the given connections and union the connected components of each pair of computers using the Disjoint Set data structure.
- After this, we count the number of disconnected components using the findUPar function of the Disjoint Set data structure.
- The minimum number of times we need to add connections to make all computers connected is the number of disconnected components minus one.
COMPLEXITY ANALYSIS:
- The time complexity of this approach is O(E + V), where E is the number of connections and V is the number of computers. The log V factor is due to the unionBySize operation in the Disjoint Set data structure.
- The space complexity is O(V) for the Disjoint Set data structure and other data structures.
CODE:
*/
class DisjointSet {
vector<int> parent, size;
public:
DisjointSet(int n) {
parent.resize(n);
size.resize(n, 1);
for (int i = 0; i < n; i++)
parent[i] = i;
}
int findUPar(int node) {
if (parent[node] == node)
return node;
return parent[node] = findUPar(parent[node]);
}
void unionBySize(int u, int v) {
int uP = findUPar(u), vP = findUPar(v);
int uPSize = size[uP], vPSize = size[vP];
if (uPSize > vPSize) {
parent[vP] = uP;
size[uP] += vPSize;
} else {
parent[uP] = vP;
size[vP] += uPSize;
}
}
int disconnected(int n) {
int cnt = 0;
for (int i = 0; i < n; i++) {
if (parent[i] == i)
cnt++;
}
return cnt;
}
};
int makeConnected(int n, vector<vector<int>>& connections) {
if (n - 1 > connections.size())
return -1;
DisjointSet djs(n);
for (auto c : connections) {
int u = c[0], v = c[1];
if (djs.findUPar(u) != djs.findUPar(v))
djs.unionBySize(u, v);
}
int ans = djs.disconnected(n);
return ans - 1;
}
/*
QUESTION:
On a 2D plane, we place n stones at some integer coordinate points. Each coordinate point may have at most one stone.
A stone can be removed if it shares either the same row or the same column as another stone that has not been removed.
Given an array stones of length n where stones[i] = [xi, yi] represents the location of the ith stone, return the largest possible number of stones that can be removed.
Example 1:
Input: stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
Output: 5
Explanation: One way to remove 5 stones is as follows:
1. Remove stone [2,2] because it shares the same row as [2,1].
2. Remove stone [2,1] because it shares the same column as [0,1].
3. Remove stone [1,2] because it shares the same row as [1,0].
4. Remove stone [1,0] because it shares the same column as [0,0].
5. Remove stone [0,1] because it shares the same row as [0,0].
Stone [0,0] cannot be removed since it does not share a row/column with another stone still on the plane.
APPROACH:
To solve this problem, we can use the Disjoint Set data structure to group stones that share the same row or column.
- First, we find the minimum and maximum row and column values to calculate the size of the grid.
- Then, we iterate through the stones and perform the union operation for stones that share the same row or column.
- Finally, we count the number of disjoint sets and subtract it from the total number of stones to get the maximum number of stones that can be removed.
COMPLEXITY ANALYSIS:
Let n be the number of stones and m be the size of the grid.
- Building the Disjoint Set: O(n)
- Finding the number of disjoint sets: O(n)
- Overall Time Complexity: O(n)
- Space Complexity: O(n) for the Disjoint Set data structure
CODE:-
*/
class DisjointSet {
vector<int> parent, size;
public:
DisjointSet(int n) {
size.resize(n, 1);
parent.resize(n);
for (int i = 0; i < n; i++)
parent[i] = i;
}
int findUPar(int node) {
if (parent[node] == node)
return node;
return parent[node] = findUPar(parent[node]);
}
void unionBySize(int u, int v) {
int uP = findUPar(u), vP = findUPar(v);
int uSize = size[uP], vSize = size[vP];
if (uP == vP)
return;
if (uSize > vSize) {
parent[vP] = uP;
size[uP] += vSize;
} else {
parent[uP] = vP;
size[vP] += uSize;
}
}
int countComponents(int n) {
int cnt = 0;
for (int i = 0; i < n; i++) {
if (parent[i] == i)
cnt++;
}
return cnt;
}
};
int makeConnected(int n, vector<vector<int>>& connections) {
if (n - 1 > connections.size())
return -1;
DisjointSet djs(n);
for (auto c : connections) {
int u = c[0], v = c[1];
if (djs.findUPar(u) != djs.findUPar(v))
djs.unionBySize(u, v);
}
int components = djs.countComponents(n);
return components - 1;
}
/*
QUESTION:-
Given a list of accounts where each element accounts[i] is a list of strings, where the first element accounts[i][0] is a name, and the rest of the elements are emails representing emails of the account.
Now, we would like to merge these accounts. Two accounts definitely belong to the same person if there is some common email to both accounts. Note that even if two accounts have the same name, they may belong to different people as people could have the same name. A person can have any number of accounts initially, but all of their accounts definitely have the same name.
After merging the accounts, return the accounts in the following format: the first element of each account is the name, and the rest of the elements are emails in sorted order. The accounts themselves can be returned in any order.
Approach:
1. We use a disjoint-set data structure to group accounts that belong to the same person based on their common emails.
2. Create a disjoint-set and an unordered map to store email to account index mapping.
3. Iterate through the list of accounts, for each account, iterate through its emails, and add them to the unordered map with the corresponding account index as the value.
4. Iterate through the unordered map and union accounts that share common emails using the disjoint-set.
5. Create a set for each group of accounts and add the emails of each account to the corresponding set.
6. Iterate through the disjoint-set and create the final merged accounts list by grouping emails for each account based on their root in the disjoint-set.
Complexity Analysis:
- Let n be the number of accounts and m be the average number of emails per account.
- The time complexity of this approach is O(n*m*α(n)), where α(n) is the inverse Ackermann function, which grows very slowly and is nearly constant.
- The space complexity is O(n*m) to store the emails and their corresponding account indices in the unordered map, and O(n*m) to store the merged accounts in the final result.
*/
class DisjointSet {
public:
vector<int> parent, size;
DisjointSet(int n) {
size.resize(n, 1);
parent.resize(n);
for (int i = 0; i < n; i++)
parent[i] = i;
}
int findUPar(int node) {
if (parent[node] == node)
return node;
return parent[node] = findUPar(parent[node]);
}
void unionSize(int u, int v) {
int uP = findUPar(u), vP = findUPar(v);
int uS = size[uP], vS = size[vP];
if (uS > vS) {
parent[vP] = uP;
size[uP] += vS;
} else {
parent[uP] = vP;
size[vP] += uS;
}
}
};
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
unordered_map<string, vector<int>> emailToAccountIndex;
int n = accounts.size();
DisjointSet djs(n);
for (int u = 0; u < n; u++) {
for (int i = 1; i < accounts[u].size(); i++) {
emailToAccountIndex[accounts[u][i]].push_back(u);
}
}
for (auto [email, users] : emailToAccountIndex) {
for (int i = 1; i < users.size(); i++) {
if (djs.findUPar(users[i - 1]) != djs.findUPar(users[i]))
djs.unionSize(users[i - 1], users[i]);
}
}
set<string> userMail[n];
for (int i = 0; i < n; i++) {
for (int m = 1; m < accounts[i].size(); m++) {
userMail[djs.findUPar(i)].insert(accounts[i][m]);
}
}
vector<vector<string>> ans;
for (int i = 0; i < n; i++) {
if (userMail[i].empty()) continue;
vector<string> temp;
temp.push_back(accounts[i][0]);
for (auto m : userMail[i])
temp.push_back(m);
ans.push_back(temp);
}
return ans;
}
/*
QUESTION:-
You are given an n x m 2D matrix, and an array of size k denoting the number of operations. The matrix elements are 0 if there is water or 1 if there is land. Originally, the 2D matrix is all 0, which means there is no land in the matrix. The array has k operator(s) and each operator has two integers A[i][0], A[i][1], which means that you can change the cell matrix[A[i][0]][A[i][1]] from sea to an island. Return how many islands are there in the matrix after each operation. You need to return an array of size k.
Note: An island means a group of 1s such that they share a common side.
Example 1:
Input: n = 4, m = 5, k = 4, A = {{1,1},{0,1},{3,3},{3,4}}
Output: 1 1 2 2
Explanation:
0. 00000
00000
00000
00000
1. 00000
01000
00000
00000
2. 01000
01000
00000
00000
3. 01000
01000
00000
00010
4. 01000
01000
00000
00011
*/
/*
Approach:
1. Create a disjoint set to represent islands.
2. Initialize an empty grid to track the islands.
3. For each operation (i.e., changing sea to land), do the following:
a. If the cell is not already land, mark it as land and increment the island count.
b. For the neighboring cells that are land, check if they belong to the same island or not.
- If they don't belong to the same island, union the sets and decrement the island count.
4. Return the island count after each operation as the result.
Complexity Analysis:
- The time complexity of each operation is O(4 * alpha(n*m)) due to the union operation, where alpha(n*m) is the inverse Ackermann function.
- Since there are k operations, the overall time complexity is O(k * alpha(n*m)).
- The space complexity is O(n*m) for storing the grid and the disjoint set.
CODE:-
*/
class DisjointSet {
vector<int> parent, size;
public:
DisjointSet(int n) {
size.resize(n, 1);
parent.resize(n);
for (int i = 0; i < n; i++) parent[i] = i;
}
int findUPar(int node) {
if (parent[node] == node) return node;
return parent[node] = findUPar(parent[node]);
}
void unionSize(int u, int v) {
int up = findUPar(u), vp = findUPar(v);
int us = size[up], vs = size[vp];
if (us > vs) {
parent[vp] = up;
size[up] += vs;
} else {
parent[up] = vp;
size[vp] += us;
}
}
};
bool isLand(int x, int y, vector<vector<int>>& grid) {
return (x >= 0 && x < grid.size() && y >= 0 && y < grid[0].size() && grid[x][y] == 1);
}
vector<int> numOfIslands(int n, int m, vector<vector<int>>& operators) {
vector<int> ans;
int islands = 0;
DisjointSet djs(n * m);
vector<vector<int>> grid(n, vector<int>(m, 0));
vector<int> dr = { -1, 1, 0, 0 };
vector<int> dc = { 0, 0, -1, 1 };
for (auto op : operators) {
int x = op[0], y = op[1];
if (grid[x][y] != 1) {
grid[x][y] = 1;
islands++;
int id = x * m + y;
for (int i = 0; i < 4; i++) {
int nx = x + dr[i], ny = y + dc[i];
if (isLand(nx, ny, grid)) {
int nid = nx * m + ny;
if (djs.findUPar(id) != djs.findUPar(nid)) {
djs.unionSize(id, nid);
islands--;
}
}
}
}
ans.push_back(islands);
}
return ans;
}
/*
QUESTION:-
You are given an n x n binary matrix grid. You are allowed to change at most one 0 to be 1.
Return the size of the largest island in grid after applying this operation.
An island is a 4-directionally connected group of 1s.
Approach:
1. Create a disjoint set to represent islands and initialize it with all cells.
2. For each 1 cell, union it with its 4-directionally connected 1 cells in the disjoint set.
3. Then, traverse the grid to find each 0 cell and count the size of connected islands that can be formed by changing this 0 to 1.
4. Update the answer with the maximum island size found so far.
5. Return the maximum island size.
Complexity Analysis:
- The time complexity of creating the disjoint set is O(n^2) as we need to initialize it with all cells.
- The time complexity of the first pass (connecting 1 cells) is also O(n^2) as we traverse the entire grid once.
- The time complexity of the second pass (counting island size for 0 cells) is O(n^2) as we again traverse the entire grid.
- The time complexity of finding the maximum island size using the disjoint set is O(n^2) as we iterate through all cells.
- Overall, the time complexity of the solution is O(n^2).
- The space complexity is O(n^2) due to the disjoint set data structure.
CODE:-
*/
class DisjointSet {
public:
vector<int> parent, size;
DisjointSet(int n) {
size.resize(n, 1);
parent.resize(n);
for (int i = 0; i < n; i++)
parent[i] = i;
}
int findUPar(int node) {
if (parent[node] == node)
return node;
return parent[node] = findUPar(parent[node]);
}
void unionBySize(int u, int v) {
int uP = findUPar(u), vP = findUPar(v);
int uSize = size[uP], vSize = size[vP];
if (uSize > vSize) {
parent[vP] = uP;
size[uP] += vSize;
} else {
parent[uP] = vP;
size[vP] += uSize;
}
}
};
bool isLand(int i, int j, vector<vector<int>>& grid){
return (i >= 0 && i < grid.size() && j >= 0 && j < grid.size() && grid[i][j] == 1);
}
int largestIsland(vector<vector<int>>& grid) {
int n = grid.size();
vector<int> dr = {1, -1, 0, 0};
vector<int> dc = {0, 0, 1, -1};
DisjointSet djs(n * n);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
int id = i * n + j;
for (int d = 0; d < 4; d++) {
int ni = i + dr[d], nj = j + dc[d];
if (isLand(ni, nj, grid)) {
int nid = ni * n + nj;
if (djs.findUPar(id) != djs.findUPar(nid))
djs.unionBySize(id, nid);
}
}
}
}
}
int ans = INT_MIN;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 0) {
unordered_set<int> st;
for (int d = 0; d < 4; d++) {
int ni = i + dr[d], nj = j + dc[d];
if (isLand(ni, nj, grid)) {
int nid = ni * n + nj;
st.insert(djs.findUPar(nid));
}
}
int siz = 0;
for (auto u : st)
siz += djs.size[u];
ans = max(ans, siz + 1);
}
}
}
for (int cellNo = 0; cellNo < n * n; cellNo++) {
ans = max(ans, djs.size[djs.findUPar(cellNo)]);
}
return ans;
}
/*
QUESTION:-
You are given an n x n integer matrix grid where each value grid[i][j] represents the elevation at that point (i, j).
The rain starts to fall. At time t, the depth of the water everywhere is t. You can swim from a square to another 4-directionally adjacent square if and only if the elevation of both squares individually are at most t. You can swim infinite distances in zero time. Of course, you must stay within the boundaries of the grid during your swim.
Return the least time until you can reach the bottom right square (n - 1, n - 1) if you start at the top left square (0, 0).
Approach:
1. We use a priority queue (min heap) to keep track of the cells in increasing order of their elevations.
2. Start from the top left cell (0, 0) and add it to the priority queue with its elevation as the key.
3. Mark this cell as visited.
4. While the priority queue is not empty, pop the cell with the minimum elevation.
5. Update the answer with the maximum of the current elevation and the answer so far.
6. Check the 4-directionally adjacent cells of the current cell.
7. If the adjacent cell is within the boundaries and not visited, add it to the priority queue with its elevation as the key.
8. Mark the adjacent cell as visited.
9. Continue the process until we reach the bottom right cell (n-1, n-1).
10. The answer at this point will be the least time until we can reach the bottom right cell.
Complexity Analysis:
- We need to visit all cells of the grid, so the time complexity of the solution is O(n^2).
- We use a priority queue to store at most n^2 cells, so the space complexity is also O(n^2).
CODE:-
*/
typedef pair<int, pair<int, int>> dup;
int swimInWater(vector<vector<int>>& grid) {
int n = grid.size();
vector<vector<bool>> vis(n, vector<bool>(n));
priority_queue<dup, vector<dup>, greater<dup>> pq;
pq.push({grid[0][0], {0, 0}});
vis[0][0] = true;
vector<int> dr = {-1, 1, 0, 0};
vector<int> dc = {0, 0, -1, 1};
int ans = 0;
while (!pq.empty()) {
auto [uwt, cord] = pq.top();
pq.pop();
int x = cord.first, y = cord.second;
ans = max(ans, uwt);
if (x == n - 1 && y == n - 1) return ans;
for (int i = 0; i < 4; i++) {
int nx = x + dr[i], ny = y + dc[i];
if (nx >= 0 && nx < n && ny >= 0 && ny < n && !vis[nx][ny]) {
vis[nx][ny] = true;
pq.push({grid[nx][ny], {nx, ny}});
}
}
}
return ans;
}
6. Other Advanced Graph Algorithms¶
These algorithms address specialized graph problems such as connectivity and articulation points.
/*
QUESTION:-
There are n servers numbered from 0 to n - 1 connected by undirected server-to-server connections forming a network where connections[i] = [ai, bi] represents a connection between servers ai and bi. Any server can reach other servers directly or indirectly through the network.
A critical connection is a connection that, if removed, will make some servers unable to reach some other server.
Return all critical connections in the network in any order.
Approach:
1. We can use Tarjan's algorithm to find the critical connections in the network.
2. Tarjan's algorithm is used to find bridges in an undirected graph, which are exactly the critical connections.
3. We perform a depth-first search (DFS) on the graph and keep track of the timestamp (time) when each node is visited.
4. We also maintain two arrays, tin and low, to store the timestamp of each node and the lowest timestamp reachable from the node using a back edge or a cross edge, respectively.
5. During the DFS, if we encounter an edge (u, v) such that low[v] > tin[u], it means the edge (u, v) is a critical connection (bridge).
6. We add all such critical connections to the result.
Complexity Analysis:
- Let n be the number of servers and m be the number of connections in the network.
- The time complexity of this approach is O(n + m) since we perform a single DFS on the graph.
- The space complexity is O(n + m) to store the graph adjacency list and O(n) for the auxiliary arrays.
*/
int time;
void dfs(int node, int parent, int tin[], int low[], vector<int> adj[], vector<vector<int>>& ans, vector<bool>& vis) {
vis[node] = true;
low[node] = tin[node] = time;
time++;
for (auto v : adj[node]) {
if (v == parent) continue;
if (vis[v]) {
low[node] = min(low[node], low[v]);
} else {
dfs(v, node, tin, low, adj, ans, vis);
low[node] = min(low[node], low[v]);
if (low[v] > tin[node])
ans.push_back({v, node});
}
}
}
vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
time = 0;
vector<int> adj[n];
for (auto c : connections) {
adj[c[0]].push_back(c[1]);
adj[c[1]].push_back(c[0]);
}
vector<vector<int>> ans;
vector<bool> vis(n);
int tin[n], low[n];
dfs(0, -1, tin, low, adj, ans, vis);
return ans;
}
/*
QUESTION:-
Given a Directed Graph with V vertices (Numbered from 0 to V-1) and E edges, Find the number of strongly connected components in the graph.
Approach:
1. We can use Kosaraju's algorithm to find the number of strongly connected components (SCCs) in a directed graph.
2. Kosaraju's algorithm performs two DFS traversal on the graph to find SCCs.
3. In the first DFS traversal, we find the order of vertices in which they finish their DFS traversal (topological ordering).
4. In the second DFS traversal, we visit the vertices in reverse order of their finish times (based on the first DFS traversal) and mark the SCCs.
5. The number of SCCs will be the number of times we perform the second DFS traversal and find a new SCC.
Complexity Analysis:
- Let V be the number of vertices and E be the number of edges in the directed graph.
- The time complexity of Kosaraju's algorithm is O(V + E) as we perform two DFS traversals.
- The space complexity is O(V + E) to store the adjacency list and O(V) for the auxiliary arrays.
*/
void topo(int node, vector<bool>& vis, vector<vector<int>>& adj, stack<int>& st) {
vis[node] = true;
for (auto v : adj[node]) {
if (!vis[v]) {
topo(v, vis, adj, st);
}
}
st.push(node);
}
void dfs(int node, vector<bool>& vis, vector<int> revadj[]) {
vis[node] = true;
for (auto v : revadj[node]) {
if (!vis[v]) {
dfs(v, vis, revadj);
}
}
}
int kosaraju(int V, vector<vector<int>>& adj) {
stack<int> st;
vector<bool> vis(V);
for (int i = 0; i < V; i++) {
if (!vis[i])
topo(i, vis, adj, st);
}
vector<int> revadj[V];
for (int i = 0; i < V; i++) {
for (auto j : adj[i]) {
revadj[j].push_back(i);
}
}
int ans = 0;
vector<bool> visi(V);
while (!st.empty()) {
int node = st.top();
st.pop();
if (!visi[node]) {
dfs(node, visi, revadj);
ans++;
}
}
return ans;
}