Skip to lab content

WEEK 1 · ADVANCED DATA STRUCTURES LAB

Disjoint sets — Find and Union

B.Tech CSE & Allied Branches · II Year, I Semester · AR25C173 tested sample runs

THE SYLLABUS TASK

What you need to solve

Represent disjoint subsets and perform Find and Union operations on the sets.
Input format & conventions

First enter n (1–100). Elements are numbered 0 through n−1. Commands: 1 x y = union; 2 x = find; 3 = display each element’s representative; 0 = exit.

UNDERSTAND THE IDEA

Explanation

A disjoint-set forest groups elements without storing a separate list for each set. Initially every element is its own root: parent[i] = i.

Find follows parent links until it reaches a root. Path compression rewrites links on the return journey so future calls reach that root more directly.

Union first finds both roots. Union by rank attaches the lower-rank root below the higher-rank root; equal ranks require increasing the surviving root’s rank. Representatives are implementation choices, not necessarily the smallest elements.

TURN THE IDEA INTO STEPS

Step-by-step algorithm

  1. Create n singleton roots with rank zero.
  2. For Find, follow parents and compress the path back to the root.
  3. For Union, find both roots and do nothing if they already match.
  4. Attach the lower-rank tree; increase rank only when equal-rank roots are combined.
  5. Continue commands until 0; reject element indexes outside 0…n−1.

SEE THE CONTROL FLOW

Flowcharts

Diamonds show decisions; labeled arrows select a branch. Returning arrows repeat work. Helper-function steps group the operations explained above and in the code. These charts focus on the named core operation. On a phone, scroll within the diagram or open the full-size version.

Find with path compression

Find with path compression

Open full-size flowchart ↗

Union by rank

Union by rank

Open full-size flowchart ↗

C17

Complete C program

Download .c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

/* Read one entire token; reject malformed, oversized and out-of-range input. */
int readToken(char text[], size_t capacity) {
    int ch; size_t used = 0; int fits = 1;
    while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
    if (ch == EOF) return 0;
    do {
        if (ch == 0) fits = 0;
        if (used + 1 < capacity) text[used++] = (char)ch;
        else fits = 0;
        ch = getchar();
    } while (ch != EOF && !isspace((unsigned char)ch));
    text[used] = '\0';
    return fits;
}
int readInt(int *value) {
    char token[64], *end;
    if (!readToken(token, sizeof token)) return 0;
    errno = 0; long result = strtol(token, &end, 10);
    if (errno || end == token || *end || result < -1000000 || result > 1000000) return 0;
    *value = (int)result; return 1;
}
int invalid(void) { puts("Invalid or incomplete input."); return 1; }

#define MAX_N 100
int parent[MAX_N], rankValue[MAX_N];
int findSet(int x) {
    if (parent[x] != x) parent[x] = findSet(parent[x]);
    return parent[x];
}
void unionSets(int x, int y) {
    int a = findSet(x), b = findSet(y);
    if (a == b) return;
    if (rankValue[a] < rankValue[b]) parent[a] = b;
    else {
        parent[b] = a;
        if (rankValue[a] == rankValue[b]) ++rankValue[a];
    }
}
/* First n (1..100); then 1 x y: union, 2 x: find, 3: display, 0: exit. */
int main(void) {
    int n, command, x, y;
    if (!readInt(&n) || n < 1 || n > MAX_N) return invalid();
    for (int i = 0; i < n; ++i) { parent[i] = i; rankValue[i] = 0; }
    while (readInt(&command)) {
        if (command == 0) return 0;
        if (command == 3) {
            printf("Representatives:");
            for (int i = 0; i < n; ++i) printf(" %d", findSet(i));
            putchar('\n'); continue;
        }
        if (!readInt(&x) || x < 0 || x >= n) return invalid();
        if (command == 1) {
            if (!readInt(&y) || y < 0 || y >= n) return invalid();
            unionSets(x,y); puts("Union complete.");
        } else if (command == 2) printf("Find(%d): %d\n",x,findSet(x));
        else return invalid();
    }
    return invalid();
}

Local compile: gcc -std=c17 disjoint-sets.c -o program -lm

Open in C compiler transfers the source. Copy the sample input into the compiler’s input box separately before running.

UNDERSTAND THE CODE

Line-by-line explanation

Follow the exact source above, from input handling through the algorithm and its result. Line numbers count blank lines; related statements are grouped into steps. Start with main to follow execution, then open the functions it calls.

Start with main → · Algorithm and data structure →

Lines 1–5Include input, conversion and validation declarations
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

stdio.h supplies terminal input/output. stdlib.h declares conversion and allocation functions. string.h supplies string and memory utilities. ctype.h supplies isspace, and errno.h supplies conversion error reporting. Headers declare facilities; including them does not execute an operation.

Lines 7–9Define a complete-token reader and initialise its state
/* Read one entire token; reject malformed, oversized and out-of-range input. */
int readToken(char text[], size_t capacity) {
    int ch; size_t used = 0; int fits = 1;

readToken receives a writable buffer and its capacity. ch is int so it can represent EOF as well as input characters. used counts stored characters, and fits starts as a success flag. One slot must remain for the null terminator.

Lines 10–11Skip leading whitespace and recognise end-of-file
    while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
    if (ch == EOF) return 0;

Read until a non-whitespace character appears. The unsigned-char cast supplies an allowed value to isspace. Return 0 if EOF arrives before a token begins.

Lines 12–15Collect a token without silently truncating it
    do {
        if (ch == 0) fits = 0;
        if (used + 1 < capacity) text[used++] = (char)ch;
        else fits = 0;

The do body handles the character already read. An embedded zero byte marks the token invalid. Store a character only while there is room for it and a null terminator; otherwise clear fits. Continue reading even after the buffer fills.

Lines 16–20Consume the full token and terminate the buffer
        ch = getchar();
    } while (ch != EOF && !isspace((unsigned char)ch));
    text[used] = '\0';
    return fits;
}

Read through the next whitespace or EOF, add a null character after the stored prefix, then return fits. A too-long token returns 0 rather than pretending that its stored prefix is the complete token.

Lines 21–23Wrap token input as a checked integer read
int readInt(int *value) {
    char token[64], *end;
    if (!readToken(token, sizeof token)) return 0;

value is an output address. A 64-slot local buffer receives the token; a failed token read returns 0 before attempting conversion.

Lines 24–25Convert base-ten text and check the whole result
    errno = 0; long result = strtol(token, &end, 10);
    if (errno || end == token || *end || result < -1000000 || result > 1000000) return 0;

Reset errno before strtol. &end receives the stopping address. Reject conversion errors, no converted digits, leftover characters, or values outside -1,000,000 to 1,000,000. Thus 12x is not accepted as the integer 12.

Lines 26–28Return the accepted value or a standard error status
    *value = (int)result; return 1;
}
int invalid(void) { puts("Invalid or incomplete input."); return 1; }

Store a validated integer through *value and return 1 from readInt. invalid is a separate helper that prints the error and returns 1 as main’s failure status; the same number has different roles in these two function contracts.

Lines 30–31Store each element’s parent and rank
#define MAX_N 100
int parent[MAX_N], rankValue[MAX_N];

The fixed arrays have room for 100 elements. parent links represent a forest of sets. rankValue is a balancing estimate; after path compression it is not necessarily the current exact tree height.

Lines 32–35Find a representative and compress the path
int findSet(int x) {
    if (parent[x] != x) parent[x] = findSet(parent[x]);
    return parent[x];
}

A root satisfies parent[x] == x. Otherwise recursively find the parent’s representative and save that root back into parent[x]. Return the representative. This shortens future searches for the same element.

Lines 36–38Find both roots before joining sets
void unionSets(int x, int y) {
    int a = findSet(x), b = findSet(y);
    if (a == b) return;

unionSets first obtains representatives a and b. If they are already equal, the elements already belong to one set, so return without changing rank or parent links.

Lines 39–44Attach the lower-rank root below the higher-rank root
    if (rankValue[a] < rankValue[b]) parent[a] = b;
    else {
        parent[b] = a;
        if (rankValue[a] == rankValue[b]) ++rankValue[a];
    }
}

If a has lower rank, its parent becomes b. Otherwise attach b below a; when their ranks were equal, increase a’s rank. Only roots are joined, preserving a valid parent forest.

Lines 45–49Read the set size and initialise singleton sets
/* First n (1..100); then 1 x y: union, 2 x: find, 3: display, 0: exit. */
int main(void) {
    int n, command, x, y;
    if (!readInt(&n) || n < 1 || n > MAX_N) return invalid();
    for (int i = 0; i < n; ++i) { parent[i] = i; rankValue[i] = 0; }

The comment lists union, find, display and exit commands. Require n from 1 to 100. Each element i initially represents itself, with parent[i] = i and rank zero.

Lines 50–56Process exit or display without extra operands
    while (readInt(&command)) {
        if (command == 0) return 0;
        if (command == 3) {
            printf("Representatives:");
            for (int i = 0; i < n; ++i) printf(" %d", findSet(i));
            putchar('\n'); continue;
        }

Command 0 returns success. Command 3 prints findSet(i) for every element; these calls can also compress paths. continue skips operand reading and begins the next command.

Lines 57–60Validate element indexes and perform union
        if (!readInt(&x) || x < 0 || x >= n) return invalid();
        if (command == 1) {
            if (!readInt(&y) || y < 0 || y >= n) return invalid();
            unionSets(x,y); puts("Union complete.");

Other commands require x within 0 through n - 1. Union command 1 additionally reads and validates y, then joins the two sets. No array lookup uses a rejected element index.

Lines 61–65Report find or reject an invalid command stream
        } else if (command == 2) printf("Find(%d): %d\n",x,findSet(x));
        else return invalid();
    }
    return invalid();
}

Command 2 prints x’s representative. Another command returns the standard error. If command reading ends before explicit command 0, the program also reports invalid/incomplete input.

FOLLOW THE VALUES

Dry run

  1. For n = 5, parents initially represent five separate sets.
  2. Union(0,2) makes root 0 represent {0,2}.
  3. Union(4,2) attaches 4 below the already established root 0.
  4. Union(3,1) makes root 3 represent {3,1}. Find(4) is 0 and Find(1) is 3.

CHECK THE BEHAVIOUR

Sample input & output

These outputs were captured by running the exact C source shown above. Commands use the stated input format; the programs omit repeated menu prompts so the same sample can be pasted and run.

Sample 1

INPUT
5
1 0 2
1 4 2
1 3 1
2 4
2 1
3
0
OUTPUT
Union complete.
Union complete.
Union complete.
Find(4): 0
Find(1): 3
Representatives: 0 3 0 3 0

Sample 2

INPUT
3
3
1 1 1
2 1
0
OUTPUT
Representatives: 0 1 2
Union complete.
Find(1): 1

Sample 3

INPUT
4
1 0 1
1 2 3
1 1 3
3
0
OUTPUT
Union complete.
Union complete.
Union complete.
Representatives: 0 0 0 0

Common mistakes

  • Do not connect arbitrary elements without first finding their roots.
  • Do not interpret the representative as the set’s smallest value.
  • Do not claim the combined optimization bound for the manual’s path-compression-only code.

WHY THIS GROWTH RATE?

Time and space complexity

Initialization O(n); Find/Union amortized O(α(n)); stored arrays O(n).

Creating the singleton sets touches n entries once. Union performs two finds and a constant number of link/rank updates.

Path compression together with union by rank gives an inverse-Ackermann amortized bound over a sequence of operations. It is extremely small in practice, but this is not a claim that every individual call is worst-case O(1).

A single first Find can follow O(log n) links in a rank-balanced forest. Display performs n finds and is O(n α(n)) amortized. The sample reserves arrays for 100 elements; O(n) describes the growing problem model.

Big-O describes growth in the stated input quantity. The educational limits keep examples safe; complexity discussions state which bounds depend on a generalized input size. Arithmetic on the accepted native numeric types is treated as constant-time.

PREPARE FOR YOUR LAB VIVA

Related viva questions & answers

6 focused questions

Try answering aloud, then expand the answer to check your reasoning. These questions focus on the concepts used in this program.

Why does a root satisfy parent[x] == x?

It is the stopping convention for a set representative. A self-parent means Find has reached the top of that element’s forest rather than an ordinary parent link.

What exactly is changed by path compression?

After finding the root, the function assigns each visited node’s parent directly to that root. Membership stays the same, but subsequent parent paths become shorter.

Why are ranks compared only for roots during Union?

Rank describes the construction of an entire rooted tree. Merging through its root preserves the forest; attaching an interior node could move only part of a set.

Does rank still equal the current height after path compression?

Not necessarily. Compression can reduce the actual height without reducing the stored rank. Rank remains a useful historical upper bound for choosing the surviving root.

What happens when Union receives elements already in one set?

Their roots compare equal, so no link or rank is changed. This prevents a redundant merge from introducing an incorrect parent relationship.

How can this structure detect a cycle while adding graph edges?

If both endpoints already have the same representative, they are already connected. Adding that edge would close a cycle; otherwise Union joins their components.