Open In App

Dynamic Connectivity | Set 2 (DSU with Rollback)

Last Updated : 31 Mar, 2023
Improve
Improve
Like Article
Like
Save
Share
Report

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 <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)


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) 



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads