Open In App

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 – 

DSU with Rollback



The DSU with rollback is performed following the below steps where the history of a DSU can be stored using stacks

The code for the generic implementation is as follows:




#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;
}




/*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.




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.




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




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: 

Below is the implementation of the above approach: 




// 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 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.




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)

Auxiliary Space: O(N+Q) 


Article Tags :