GFG App
Open App
Browser
Continue

# 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 –

1. Add an edge between nodes a and b
2. 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 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 using namespace std;   int N, Q, ans[10];   // Components and size of the stack int nc, sz; map, vector > > 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 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 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)

Output

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)

My Personal Notes arrow_drop_up