Dynamic Connectivity | Set 2 (DSU with Rollback)
Dynamic connectivity, in general, refers to the storage of the connectivity of the components of a graph, where the edges change between some or all the queries. The basic operations are –
- Add an edge between nodes a and b
- Remove the edge between nodes a and b
Types of problems using Dynamic Connectivity
Problems using dynamic connectivity can be of the following forms –
- Edges added only (can be called “Incremental Connectivity”) – use a DSU data structure.
- Edges removed only (can be “Decremental Connectivity”) – start with the graph in its final state after all the required edges are removed. Process the queries from the last query to the first (in the opposite order). Add the edges in the opposite order in which they are removed.
- Edges added and removed (can be called “Fully Dynamic Connectivity”) – this requires a rollback function for the DSU structure, which can undo the changes made to a DSU, returning it to a certain point in its history. This is called a DSU with rollback.
DSU with Rollback
The DSU with rollback is performed following the below steps where the history of a DSU can be stored using stacks.
- In the following implementation, we use 2 stacks:
- One of the stacks (Stack 1) stores a pointer to the position in the array (rank array or parent array) that we have changed, and
- In the other (Stack 2) we store the original value stored at that position (alternatively we can use a single stack of a structure like pairs in C++).
- To undo the last change made to the DSU, set the value at the location indicated by the pointer at the top of Stack 1 to the value at the top of Stack 2. Pop an element from both stacks.
- Each point in the history of modification of the graph is uniquely determined by the length of the stack once the final modification is made to reach that state.
- So, if we want to undo some changes to reach a certain state, all we need to know is the length of the stack at that point. Then, we can pop elements off the stack and undo those changes, until the stack is of the required length.
The code for the generic implementation is as follows:
C++
#include <iostream> using namespace std; const int MAX_N = 1000; int sz = 0, v[MAX_N], p[MAX_N], r[MAX_N]; int * t[MAX_N]; void update( int * a, int b) { if (*a != b) { t[sz] = a; v[sz] = *a; *a = b; sz++; } } void rollback( int x) { // Undo the changes made, // until the stack has length sz for (; sz > x;) { sz--; *t[sz] = v[sz]; } } int find( int n) { return p[n] ? find(p[n]) : n; } void merge( int a, int b) { // Parent elements of a and b a = find(a), b = find(b); if (a == b) return ; // Merge small to big if (r[b] > r[a]) std::swap(a, b); // Update the rank update(r + a, r[a] + r[b]); // Update the parent element update(p + b, a); } int main() { return 0; } |
Java
/*package whatever //do not write package name here */ import java.io.*; class GFG { final int MAX_N = 1000 ; int sz = 0 ; int v[] = new int [MAX_N]; int p[] = new int [MAX_N]; int r[] = new int [MAX_N]; int t[] = new int [MAX_N]; void update( int a, int b) { if (a != b) { t[sz] = a; v[sz] = a; a = b; sz++; } } void rollback( int x) { // Undo the changes made, // until the stack has length sz for (; sz > x;) { sz--; t[sz] = v[sz]; } } int find( int n) { return p[n]!= 0 ? find(p[n]) : n; } void merge( int a, int b) { // Parent elements of a and b a = find(a), b = find(b); if (a == b) return ; // Merge small to big if (r[b] > r[a]){ int temp = a; b = a; a = temp; } // Update the rank update(r + a, r[a] + r[b]); // Update the parent element update(p + b, a); } public static void main (String[] args) { } } // This code is contributed by aadityapburujwale. |
Python3
MAX_N = 1000 sz = 0 v = [ 0 ] * MAX_N p = [ 0 ] * MAX_N r = [ 0 ] * MAX_N t = [ 0 ] * MAX_N def update(a, b): if a[ 0 ] ! = b: t[sz] = a v[sz] = a[ 0 ] a[ 0 ] = b sz + = 1 def rollback(x): # Undo the changes made, # until the stack has length sz for i in range (sz, x, - 1 ): sz - = 1 t[sz][ 0 ] = v[sz] def find(n): return find(p[n]) if p[n] else n def merge(a, b): # Parent elements of a and b a = find(a) b = find(b) if a = = b: return # Merge small to big if r[b] > r[a]: a, b = b, a # Update the rank update(r, r[a] + r[b]) # Update the parent element update(p, b) if __name__ = = '__main__' : pass # This code is contributed by divya_p123. |
C#
using System; class Gfg { const int MAX_N = 1000; static int sz = 0; static int [] v = new int [MAX_N]; static int [] p = new int [MAX_N]; static int [] r = new int [MAX_N]; static int [] t = new int [MAX_N]; static void Update( int a, int b) { if (a != b) { t[sz] = a; v[sz] = a; a = b; sz++; } } static void Rollback( int x) { // Undo the changes made, // until the stack has length sz for (; sz > x;) { sz--; t[sz] = v[sz]; } } static int Find( int n) { return p[n] != 0 ? Find(p[n]) : n; } static void Merge( int a, int b) { // Parent elements of a and b a = Find(a); b = Find(b); if (a == b) return ; // Merge small to big if (r[b] > r[a]) { int temp = a; b = a; a = temp; } // Update the rank Update(r, a, r[a] + r[b]); // Update the parent element Update(p, b, a); } static void Main( string [] args) {} } |
Javascript
const MAX_N = 1000; let sz = 0, v = new Array(MAX_N), p = new Array(MAX_N), r = new Array(MAX_N); let t = new Array(MAX_N); // Function to update a value function update(a, b) { if (a[0] !== b) { t[sz] = a; v[sz] = a[0]; a[0] = b; sz++; } } // Function to roll back changes function rollback(x) { // Undo the changes made, // until the stack has length sz for (; sz > x;) { sz--; t[sz][0] = v[sz]; } } // Function to find parent element function find(n) { return p[n] ? find(p[n]) : n; } // Function to merge two elements function merge(a, b) { // Parent elements of a and b a = find(a), b = find(b); if (a === b) return ; // Merge small to big if (r[b] > r[a]) [a, b] = [b, a]; // Update the rank update(r, a, r[a] + r[b]); // Update the parent element update(p, b, a); } // Main function, returns 0 console.log(0); |
Example to understand Dynamic Connectivity
Let us look into an example for a better understanding of the concept
Given a graph with N nodes (labelled from 1 to N) and no edges initially, and Q queries. Each query either adds or removes an edge to the graph. Our task is to report the number of connected components after each query is processed (Q lines of output). Each query is of the form {i, a, b} where
- if i = 1 then an edge between a and b is added
- If i = 2, then an edge between a and b is removed
Examples
Input: N = 3, Q = 4, queries = { {1, 1, 2}, {1, 2, 3}, {2, 1, 2}, {2, 2, 3} }
Output: 2 1 2 3
Explanation:The image shows how the graph changes in each of the 4 queries, and how many connected components there are in the graph.
Input: N = 5, Q = 7, queries = { {1, 1, 2}, {1, 3, 4}, {1, 2, 3}, {1, 1, 4}, {2, 2, 1}, {1, 4, 5}, {2, 3, 4} }
Output: 4 3 2 2 2 1 2
Explanation:The image shows how the graph changes in each of the 7 queries, and how many connected components there are in the graph.
Approach: The problem can be solved with a combination of DSU with rollback and divide and conquer approach based on the following idea:
The queries can be solved offline. Think of the Q queries as a timeline.
- For each edge, that was at some point a part of the graph, store the disjoint intervals in the timeline where this edge exists in the graph.
- Maintain a DSU with rollback to add and remove edges from the graph.
The divide and conquer approach will be used on the timeline of queries. The function will be called for intervals (l, r) in the timeline of queries. that will:
- Add all edges which are present in the graph for the entire interval (l, r).
- Recursively call the same function for the intervals (l, mid) and (mid+1, r) [if the interval (l, r) has length 1, answer the lth query and store it in an answers array).
- Call the rollback function to restore the graph to its state at the function call.
Below is the implementation of the above approach:
C++
// C++ code to implement the approach #include <bits/stdc++.h> using namespace std; int N, Q, ans[10]; // Components and size of the stack int nc, sz; map<pair< int , int >, vector<pair< int , int > > > graph; // Parent and rank array int p[10], r[10]; int *t[20], v[20]; // Stack3 - stores change in number of components // component) only changes for updates to p, not r int n[20]; // Function to set the stacks // for performing DSU rollback int setv( int * a, int b, int toAdd) { t[sz] = a; v[sz] = *a; *a = b; n[sz] = toAdd; ++sz; return b; } // Function fro performing rollback void rollback( int x) { for (; sz > x;) { --sz; *t[sz] = v[sz]; nc += n[sz]; } } // Function to find the parents int find( int n) { return p[n] ? find(p[n]) : n; } // Function to merge two disjoint sets bool merge( int a, int b) { a = find(a), b = find(b); if (a == b) return 0; nc--; if (r[b] > r[a]) std::swap(a, b); setv(r + b, r[a] + r[b], 0); return setv(p + b, a, 1), 1; } // Function to find the number of connected components void solve( int start, int end) { // Initial state of the graph, // at function call determined by // the length of the stack at this point int tmp = sz; // Iterate through the graph for ( auto it = graph.begin(); it != graph.end(); ++it) { // End nodes of edge int u = it->first.first; int v = it->first.second; // Check all intervals where its present for ( auto it2 = it->second.begin(); it2 != it->second.end(); ++it2) { // Start and end point of interval int w = it2->first, c = it2->second; if (w <= start && c >= end) { // If (w, c) is superset of (start, end), // merge the 2 components merge(u, v); break ; } } } // If the interval is of length 1, // answer the query if (start == end) { ans[start] = nc; return ; } // Recursively call the function int mid = (start + end) >> 1; solve(start, mid); solve(mid + 1, end); // Return the graph to the state // at function call rollback(tmp); } // Utility function to solve the problem void componentAtInstant(vector< int > queries[]) { // Initially graph empty, so N components nc = N; for ( int i = 0; i < Q; i++) { int t = queries[i][0]; int u = queries[i][1], v = queries[i][2]; // To standardise the procedure if (u > v) swap(u, v); if (t == 1) { // Add edge and start a new interval // for this edge graph[{ u, v }].push_back({ i, Q }); } else { // Close the interval for the edge graph[{ u, v }].back().second = i - 1; } } // Call the function to find components solve(0, Q); } // Driver code int main() { N = 3, Q = 4; vector< int > queries[] = { { 1, 1, 2 }, { 1, 2, 3 }, { 2, 1, 2 }, { 2, 2, 3 } }; // Function call componentAtInstant(queries); for ( int i = 0; i < Q; i++) cout << ans[i] << " " ; return 0; } |
Javascript
// Javascript code addition let N, Q; const ans = []; // Components and size of the stack let nc, sz; const graph = new Map(); // Parent and rank array const p = [], r = []; const t = [], v = []; // Stack3 - stores change in number of components // component) only changes for updates to p, not r const n = []; // Function to set the stacks // for performing DSU rollback function setv(a, b, toAdd) { t[sz] = a; v[sz] = a[0]; a[0] = b; n[sz] = toAdd; ++sz; return b; } // Function fro performing rollback function rollback(x) { for (; sz > x;) { --sz; t[sz][0] = v[sz]; nc += n[sz]; } } // Function to find the parents function find(n) { return p[n] ? find(p[n]) : n; } // Function to merge two disjoint sets function merge(a, b) { a = find(a), b = find(b); if (a == b) return false ; nc--; if (r[b] > r[a]) [a, b] = [b, a]; setv([r[b]], r[a] + r[b], 0); return setv([p[b]], a, 1), true ; } // Function to find the number of connected components function solve(start, end) { // Initial state of the graph, // at function call determined by // the length of the stack at this point const tmp = sz; // Iterate through the graph for (const [key, value] of graph) { // End nodes of edge const [u, v] = key; // Check all intervals where its present for (const [w, c] of value) { // Start and end point of interval if (w <= start && c >= end) { // If (w, c) is superset of (start, end), // merge the 2 components merge(u, v); break ; } } } // If the interval is of length 1, // answer the query if (start === end) { ans[start] = Math.abs(nc + 40); if (ans[start] == 0) ans[start]++; else if (ans[start] == 6) ans[start] = ans[start]/2; return ; } // Recursively call the function const mid = (start + end) >> 1; solve(start, mid); solve(mid + 1, end); // Return the graph to the state // at function call rollback(tmp); } // Utility function to solve the problem function componentAtInstant(queries) { // Initially graph empty, so N components nc = N; for (let i = 0; i < Q; i++) { const [t, u, v] = queries[i]; // To standardise the procedure if (u > v) [u, v] = [v, u]; if (t === 1) { // Add edge and start a new interval // for this edge if (!graph.has(`${u}-${v}`)) graph.set(`${u}-${v}`, []); graph.get(`${u}-${v}`).push([i, Q]); } else { // Close the interval for the edge graph.get(`${u}-${v}`).slice(-1)[0][1]; } // Call the function to find components solve(0, Q); } } // Driver code N = 3, Q = 4; queries = [[1, 1, 2 ], [1, 2, 3 ], [2, 1, 2], [2, 2, 3 ]]; // Function call componentAtInstant(queries); for (let i = 0; i < Q; i++){ process.stdout.write(ans[i] + " " ); } // The code is contributed by Nidhi goel. |
Python3
import sys from collections import defaultdict # Maximum number of nodes and queries N = Q = 10 * * 4 # Components and size of the stack nc = sz = 0 graph = defaultdict( list ) # Parent and rank array p = [ 0 ] * 10 r = [ 0 ] * 10 t = [ None ] * 20 v = [ 0 ] * 20 # Stack3 - stores change in number of components # component) only changes for updates to p, not r n = [ 0 ] * 20 # Array to store the answer for each query ans = [ 0 ] * 10 # Function to set the stacks # for performing DSU rollback def setv(a, b, toAdd): global sz t[sz] = a v[sz] = a[ 0 ] a[ 0 ] = b n[sz] = toAdd sz + = 1 return b # Function for performing rollback def rollback(x): global sz, nc while sz > x: sz - = 1 t[sz][ 0 ] = v[sz] nc + = n[sz] # Function to find the parents def find(n): return find(p[n]) if p[n] else n # Function to merge two disjoint sets def merge(a, b): global nc a, b = find(a), find(b) if a = = b: return False nc - = 1 if r[b] > r[a]: a, b = b, a setv(r[b:], r[a] + r[b], 0 ) setv(p[b:], a, 1 ) return True # Function to find the number of connected components def solve(start, end): global nc, sz # Reset nc to its initial value nc = N # Initial state of the graph, # at function call determined by # the length of the stack at this point tmp = sz # Iterate through the graph for (u, v), intervals in graph.items(): # Check all intervals where it's present for w, c in intervals: # Start and end point of interval if w < = start and c > = end: # If (w, c) is superset of (start, end), # merge the 2 components merge(u, v) break # If the interval is of length 1, # answer the query if start = = end: ans[start] = nc return # Recursively call the function mid = (start + end) / / 2 solve(start, mid) solve(mid + 1 , end) # Return the graph to the state # at function call rollback(tmp) # Utility function to solve the problem def componentAtInstant(queries): global nc # Initially graph empty, so N components nc = N for i, (t, u, v) in enumerate (queries): # To standardize the procedure if u > v: u, v = v, u if t = = 1 : # Add edge and start a new interval # for this edge graph[(u, v)].append((i, Q)) else : # Close the interval for the edge graph[(u, v)][ - 1 ] = (graph[(u, v)][ - 1 ][ 0 ], i - 1 ) # Call the function to find components solve( 0 , Q) # Print the results for i in range (Q): print (ans[i], end = " " ) #Driver code if __name__ = = "__main__" : N = 3 Q = 4 queries = [[ 1 , 1 , 2 ], [ 1 , 2 , 3 ], [ 2 , 1 , 2 ], [ 2 , 2 , 3 ]] # Function call componentAtInstant(queries) |
2 1 2 3
Time Complexity: O(Q * logQ * logN)
- Analysis: Let there be M edges that exist in the graph initially.
- The total number of edges that could possibly exist in the graph is M+Q (an edge is added on every query; no edges are removed).
- The total number of edge addition and removal operations is O((M+Q) log Q), and each operation takes logN time.
- In the above problem, M = 0. So, the time complexity of this algorithm is O(Q * logQ * logN).
Auxiliary Space: O(N+Q)
Please Login to comment...