Day 30: Advanced Graphs and Union-Find

Day 30: Advanced Graphs and Union-Find

Welcome to Day 30 of my 100 Days of DSA challenge! Today, I delved into advanced graph techniques and the Union-Find algorithm, a powerful tool for managing disjoint sets and solving connectivity problems. From detecting cycles in graphs to merging accounts efficiently, these tasks demonstrated the versatility of Union-Find in tackling complex graph scenarios.

Check out my GitHub repository for all the solutions and progress updates at: 100 Days of DSA

Let’s explore the challenges solved today! 🚀


1. Union-Find Algorithm

This program implements the Union-Find algorithm with path compression. Each element is initialized as its own parent. The find function uses path compression to directly link elements to their set leader, reducing tree depth. The union_sets function unites two sets by attaching the smaller tree to the larger one, using ranks to maintain balanced trees. This ensures efficient operations for checking connectivity and merging sets.

Code:

#include <iostream>
using namespace std;

#define MAX 1000  // Maximum number of elements in the Union-Find structure

class UnionFind {
    private:
        int parent[MAX];  // Parent array to store the leader of each set
        int rank[MAX];    // Rank array to store the depth of trees

    public:
        UnionFind(int n) {
            for (int i = 0; i < n; i++) {
                parent[i] = i;  // Initialize each element as its own parent
                rank[i] = 0;    // Initialize rank as 0
            }
        }

        // Find the leader (root) of a set with path compression
        int find(int element) {
            if (parent[element] != element) {
                parent[element] = find(parent[element]);  // Path compression
            }
            return parent[element];
        }

        // Union of two sets by rank
        void union_sets(int x, int y) {
            int root_x = find(x);
            int root_y = find(y);

            if (root_x != root_y) {
                // Attach smaller rank tree under the root of the higher rank tree
                if (rank[root_x] < rank[root_y]) {
                    parent[root_x] = root_y;
                } else if (rank[root_x] > rank[root_y]) {
                    parent[root_y] = root_x;
                } else {
                    parent[root_y] = root_x;
                    rank[root_x]++;
                }
            }
        }

        // Check if two elements belong to the same set
        bool are_connected(int x, int y) {
            return find(x) == find(y);
        }
};

int main() {
    int n = 10; // Number of elements
    UnionFind uf(n);
    uf.union_sets(1, 2);
    uf.union_sets(2, 3);
    uf.union_sets(4, 5);
    cout << "Are 1 and 3 connected? " << (uf.are_connected(1, 3) ? "Yes" : "No") << endl;
    cout << "Are 3 and 4 connected? " << (uf.are_connected(3, 4) ? "Yes" : "No") << endl;
    uf.union_sets(3, 4);
    cout << "After union of 3 and 4, are 3 and 4 connected? " << (uf.are_connected(3, 4) ? "Yes" : "No") << endl;
    return 0;
}

Output:


2. Detect a Cycle in an Undirected Graph

This program uses the Union-Find algorithm to detect cycles in an undirected graph. Each node starts as its own parent. The find function ensures efficient root tracking with path compression. The union_sets function merges two sets by rank and checks if the nodes are already connected. If they are, a cycle is detected. The program reads the graph edges, applies union_sets, and detects a cycle if any edge connects nodes already in the same set.

Code:

#include <iostream>
using namespace std;

#define MAX 1000

class UnionFind {
    private:
        int parent[MAX]; // Parent array to track roots of elements
        int rank[MAX];   // Rank array to optimize union operation

    public:
        // Initialize the Union-Find structure
        UnionFind(int n) {
            for (int i = 0; i < n; i++) {
                parent[i] = i; // Each node is its own parent initially
                rank[i] = 0;   // All ranks start as 0
            }
        }

        // Find the root of a set, with path compression
        int find(int node) {
            if (parent[node] != node) {
                parent[node] = find(parent[node]);  // Path compression
            }
            return parent[node];
        }

        // Union by rank to merge two sets
        bool union_sets(int u, int v) {
            int root_u = find(u);
            int root_v = find(v);

            if (root_u == root_v) {
                return true;    // Cycle detected
            }

            // Union by rank
            if (rank[root_u] > rank[root_v]) {
                parent[root_v] = root_u;
            } else if (rank[root_u] < rank[root_v]) {
                parent[root_u] = root_v;
            } else {
                parent[root_v] = root_u;
                rank[root_u]++;
            }
            return false;
        }
};

int main() {
    int num_nodes, num_edges;
    cout << "Enter the number of nodes and edges: ";
    cin >> num_nodes >> num_edges;
    UnionFind uf(num_nodes);
    cout << "Enter the edges (u v):" << endl;
    for (int i = 0; i < num_edges; i++) {
        int u, v;
        cin >> u >> v;
        if (uf.union_sets(u, v)) {
            cout << "Cycle detected in the graph." << endl;
            return 0;
        }
    }
    cout << "No cycle detected in the graph." << endl;
    return 0;
}

Output:


3. Smallest String with Swaps

This program solves the "smallest string with swaps" problem using the Union-Find data structure. It identifies connected components of indices based on the given pairs, groups characters in those components, and replaces characters with the lexicographically smallest order within each component. The result is built by reconstructing the string with sorted characters from each group.

Code:

#include <iostream>
#include <string>
#include <algorithm>
#include <map>
#include <set>
using namespace std;

#define MAX 1000

class UnionFind {
    private:
        int parent[MAX];
        int rank[MAX];

    public:
        UnionFind(int size) {
            for (int i = 0; i < size; i++) {
                parent[i] = i;
                rank[i] = 0;
            }
        }

        int find(int node) {
            if (parent[node] != node) {
                parent[node] = find(parent[node]); // Path compression
            }
            return parent[node];
        }

        void union_sets(int u, int v) {
            int root_u = find(u);
            int root_v = find(v);

            if (root_u != root_v) {
                if (rank[root_u] > rank[root_v]) {
                    parent[root_v] = root_u;
                } else if (rank[root_u] < rank[root_v]) {
                    parent[root_u] = root_v;
                } else {
                    parent[root_v] = root_u;
                    rank[root_u]++;
                }
            }
        }
};

string smallest_string_with_swaps(string s, pair<int, int> pairs[], int pair_count) {
    int n = s.size();
    UnionFind uf(n);

    // Build the Union-Find structure from the pairs
    for (int i = 0; i < pair_count; i++) {
        uf.union_sets(pairs[i].first, pairs[i].second);
    }

    // Group indices by their connected components
    map<int, multiset<char>> component_chars;
    for (int i = 0; i < n; i++) {
        int root = uf.find(i);
        component_chars[root].insert(s[i]);
    }

    // Construct the smallest string
    string result = s;
    for (int i = 0; i < n; i++) {
        int root = uf.find(i);
        result[i] = *component_chars[root].begin();
        component_chars[root].erase(component_chars[root].begin());
    }

    return result;
}

int main() {
    string s = "dcab";
    pair<int, int> pairs[] = {{0, 3}, {1, 2}, {0, 2}};
    int pair_count = sizeof(pairs) / sizeof(pairs[0]);
    cout << "Smallest String: " << smallest_string_with_swaps(s, pairs, pair_count) << endl;
    return 0;
}

Output:


4. Connected Components in an Undirected Graph

This program finds the number of connected components in an undirected graph using the Union-Find data structure. It initializes each node as its own parent and uses path compression to optimize the find operation. The union_sets function merges two sets by rank. To determine connected components, it counts how many nodes are their own parents after performing all unions.

Code:

// Solve the "connected components in an undirected graph" problem using Union-Find.

#include <iostream>
using namespace std;

#define MAX 1000

class UnionFind {
    private:
        int parent[MAX];  // Array to store parent of each node
        int rank[MAX];    // Array to store rank of each node

    public:
        // Constructor to initialize the parent and rank arrays
        UnionFind(int n) {
            for (int i = 0; i < n; i++) {
                parent[i] = i;   // Each node is its own parent initially
                rank[i] = 0;     // Rank is initialized to 0
            }
        }

        // Function to find the representative of a set
        int find(int node) {
            if (parent[node] != node) {
                parent[node] = find(parent[node]);  // Path compression
            }
            return parent[node];
        }

        // Function to union two sets
        void union_sets(int u, int v) {
            int root_u = find(u);
            int root_v = find(v);

            if (root_u != root_v) {
                if (rank[root_u] > rank[root_v]) {
                    parent[root_v] = root_u;    // Attach smaller rank tree under root of higher rank tree
                } else if (rank[root_u] < rank[root_v]) {
                    parent[root_u] = root_v;
                } else {
                    parent[root_v] = root_u;
                    rank[root_u]++;
                }
            }
        }

        // Function to count connected components
        int count_connected_components(int n) {
            int count = 0;
            for (int i = 0; i < n; i++) {
                if (parent[i] == i) {   // Nodes that are their own parent are representatives
                    count++;
                }
            }
            return count;
        }
};

int main() {
    int n, edges;
    cout << "Enter the number of nodes: ";
    cin >> n;
    cout << "Enter the number of edges: ";
    cin >> edges;
    UnionFind uf(n);
    cout << "Enter the edges (u v):" << endl;
    for (int i = 0; i < edges; i++) {
        int u, v;
        cin >> u >> v;
        uf.union_sets(u, v);
    }
    cout << "Number of connected components: " << uf.count_connected_components(n) << endl;
    return 0;
}

Output:


5. Accounts Merge Problem

Code:

#include <iostream>
#include <string>
#include <map>
#include <set>
#include <unordered_map>
#include <algorithm>
using namespace std;

class UnionFind {
    private:
        int parent[1000];
        int rank[1000];

    public:
        // Constructor initializes each node as its own parent
        UnionFind(int size) {
            for (int i = 0; i < size; i++) {
                parent[i] = i;
                rank[i] = 0;
            }
        }

        // Find the root of the set containing 'x'
        int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]); // Path compression
            }
            return parent[x];
        }

        // Union two sets by rank
        void union_sets(int x, int y) {
            int root_x = find(x);
            int root_y = find(y);

            if (root_x != root_y) {
                if (rank[root_x] > rank[root_y]) {
                    parent[root_y] = root_x;
                } else if (rank[root_x] < rank[root_y]) {
                    parent[root_x] = root_y;
                } else {
                    parent[root_y] = root_x;
                    rank[root_x]++;
                }
            }
        }
};

void accounts_merge(string accounts[][2], int n) {
    unordered_map<string, int> email_to_id; // Map email to unique ID
    map<int, set<string>> id_to_emails;    // Map ID to unique emails
    UnionFind uf(n);

    // Associate each email with an account ID
    for (int i = 0; i < n; i++) {
        for (int j = 1; accounts[i][j] != ""; j++) {
            string email = accounts[i][j];
            if (email_to_id.find(email) == email_to_id.end()) {
                email_to_id[email] = i;
            } else {
                uf.union_sets(i, email_to_id[email]);
            }
        }
    }

    // Group emails by root ID
    for (auto &entry : email_to_id) {
        string email = entry.first;
        int id = uf.find(entry.second);
        id_to_emails[id].insert(email);
    }

    // Print merged accounts
    for (auto &entry : id_to_emails) {
        int id = entry.first;
        cout << accounts[id][0]; // Account name
        for (const string &email : entry.second) {
            cout << " " << email;
        }
        cout << endl;
    }
}

int main() {
    string accounts[4][2] = {
        {"John", "johnsmith@mail.com"},
        {"John", "john00@mail.com"},
        {"Mary", "mary@mail.com"},
        {"John", "johnnybravo@mail.com"}
    };

    accounts_merge(accounts, 4);
    return 0;
}

Output:


Day 30 showcased the power of the Union-Find algorithm in solving complex graph problems efficiently. From cycle detection to merging data structures, these challenges highlighted the importance of mastering Union-Find for tackling connectivity and grouping problems in graphs. 🚀