Kotlin Coding Test Course, Union Find

Hello! In this article, we will explore the Union-Find algorithm using Kotlin. The Union-Find is a method for efficiently managing a set data structure, primarily used to find and combine connected components. This algorithm is an important concept frequently employed in problems related to graph theory.

Problem Description

Here is a problem that can be solved using Union-Find. We will understand the significance of Union-Find through the problem of counting the number of connected components.

Problem: Counting Connected Components

You have a graph with N nodes and M edges. Each node is numbered from 1 to N. Given M edges as follows, write a program to count the number of connected components.

Input Format

N M
u1 v1
u2 v2
...
uM vM
    

Where N is the number of nodes, M is the number of edges, and u_i and v_i represent the endpoints of the i-th edge.

Output Format

Print the number of connected components in a single line.

Example

Input:
5 3
1 2
2 3
4 5

Output:
2
    

By examining the graph given in the example, nodes 1, 2, and 3 form one connected component, while nodes 4 and 5 form another, resulting in a total of 2 connected components.

Introduction to the Union-Find Algorithm

The Union-Find (Union-Find) or Disjoint Set Union data structure supports two main operations:

  • Find: Finds the representative element of the set to which a specific element belongs.
  • Union: Combines two sets into one.

Union-Find is primarily used to manage the connectivity of disconnected graphs, and it employs path compression and union by rank techniques for efficiency. These techniques help in quickly performing the ‘Find’ operation by maintaining a tree structure after merging.

Implementing in Kotlin

Now, let’s implement Union-Find in Kotlin. Below is the code to solve the above problem.

class UnionFind(val size: Int) {
    private val parent = IntArray(size + 1) { it } // Initialize each node's parent to itself
    private val rank = IntArray(size + 1) { 1 } // Initialize the rank of each set

    // Find operation: Path compression technique
    fun find(node: Int): Int {
        if (parent[node] != node) {
            parent[node] = find(parent[node]) // Recursively find the parent while compressing the path
        }
        return parent[node]
    }

    // Union operation: Merging sets based on rank
    fun union(node1: Int, node2: Int) {
        val root1 = find(node1)
        val root2 = find(node2)

        if (root1 != root2) {
            if (rank[root1] > rank[root2]) {
                parent[root2] = root1
            } else if (rank[root1] < rank[root2]) {
                parent[root1] = root2
            } else {
                parent[root2] = root1
                rank[root1]++
            }
        }
    }

    // Counting the number of connected components
    fun countComponents(): Int {
        val rootSet = mutableSetOf()
        for (i in 1..size) {
            rootSet.add(find(i)) // Find the root of each node and add to the set
        }
        return rootSet.size // The number of unique roots is the number of connected components
    }
}

fun main() {
    val reader = System.`in`.bufferedReader()
    val (n, m) = reader.readLine().split(" ").map { it.toInt() }
    val unionFind = UnionFind(n)

    for (i in 0 until m) {
        val (u, v) = reader.readLine().split(" ").map { it.toInt() }
        unionFind.union(u, v) // Combine the two nodes based on edge information
    }

    println(unionFind.countComponents()) // Output the number of connected components
}
    

Code Explanation

The code above implements the Union-Find data structure. To store each node’s parent, it uses the parent array, and to store the rank (height) of each set, it uses the rank array.

1. The Find method recursively finds the root of the node through path compression. In this process, all nodes directly point to the root, speeding up future searches.

2. The Union method finds the roots of two nodes and connects the lower rank set to the higher rank set. If their ranks are equal, it connects one set to the root of the other and increases the rank.

3. The countComponents method returns the number of connected components by finding the root for all nodes and counting the unique roots.

Time Complexity Analysis

Each operation of Union-Find is very fast and can be performed in nearly constant time. The average time complexity of this algorithm is as follows:

  • Find: O(α(N))
  • Union: O(α(N))

α(N) is the inverse of the Ackermann function, which grows extremely slowly. Therefore, Union-Find operates efficiently even with large datasets.

Conclusion

In this article, we learned about the basic concepts and applications of the Union-Find algorithm, as well as how to solve problems involving connected graphs. Implementing it in Kotlin not only allowed us to understand the theory but also provided practical experience, which will be beneficial for preparing for coding tests.

Since the Union-Find algorithm is an important algorithm that can be used in various fields, it is highly recommended that you familiarize yourself with it. We will also have upcoming courses on other algorithms and data structures, so please stay tuned!

I hope this article was helpful, and if you have any questions, please leave a comment!