Kotlin coding test course, depth-first search

Introduction

Hello! In this post, we will explore the Depth-First Search (DFS) algorithm. DFS is a search algorithm used in graph or tree structures that explores as deeply as possible before backtracking to adjacent nodes. It is a crucial topic often featured in coding tests; therefore, I recommend mastering it to enhance your understanding of algorithms and your problem-solving skills.

Problem Description

Problem Title: Number of Islands

In the given 2D array, ‘1’ represents land and ‘0’ represents water. Continuous ‘1’s are considered a single island. All ‘1’s that are connected horizontally or vertically to land ‘1’ are part of the same island. Calculate the number of islands in the given array.

Input Example

[
    [1, 1, 0, 0, 0],
    [0, 1, 0, 0, 1],
    [1, 0, 0, 1, 1],
    [0, 0, 0, 0, 0],
    [1, 0, 1, 0, 1]
]

Output Example

Number of islands: 5

Solution Process

This problem can be solved using the DFS algorithm. Let’s go through the steps to solve the problem.

Step 1: Graph Representation

The problem deals with a graph represented as a 2D array. Each ‘1’ represents land, and to solve this problem, we need to determine how these lands are connected.

Step 2: Implementing the DFS Algorithm

When visiting each land ‘1’ using DFS, we need to group them as a single island. To do this, we define a recursive function to visit adjacent land ‘1’s.

Step 3: Writing the Code

Now, let’s write code to implement the basic DFS algorithm. We can implement it using Kotlin as follows.

fun numIslands(grid: Array): Int {
    if (grid.isEmpty()) return 0
    var count = 0

    fun dfs(i: Int, j: Int) {
        if (i < 0 || i >= grid.size || j < 0 || j >= grid[0].size || grid[i][j] == '0') {
            return
        }
        grid[i][j] = '0' // Mark as visited
        dfs(i - 1, j) // up
        dfs(i + 1, j) // down
        dfs(i, j - 1) // left
        dfs(i, j + 1) // right
    }

    for (i in grid.indices) {
        for (j in grid[i].indices) {
            if (grid[i][j] == '1') {
                count++
                dfs(i, j) // new island found
            }
        }
    }
    return count
}

Step 4: Code Explanation

In the above code, the numIslands function takes a 2D array as a parameter and returns the number of islands.
The internal dfs function recursively visits each land, changing already visited lands to ‘0’ to prevent duplicate visits.
The double loop checks each element of the array, increasing the island count whenever a ‘1’ is found and calling dfs.

Step 5: Time Complexity and Space Complexity

The time complexity of the DFS algorithm is O(M * N), where M is the number of rows and N is the number of columns.
The space complexity is O(H), where H is the depth of the recursive call stack. In the worst case, it can be O(M + N).

Test Case Analysis

We have written a basic code based on the above description, but it is important to verify the accuracy of the code through various test cases.
Here are descriptions of some test cases.

  1. Test Case 1:
    Input:

    [[1,1,0,0],[0,1,0,0],[0,0,0,1],[1,0,0,1]]

    Expected Output: 2

  2. Test Case 2:
    Input:

    [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]

    Expected Output: 0

  3. Test Case 3:
    Input:

    [[1,1,1,1],[1,0,0,0],[1,1,1,1]]

    Expected Output: 1

Conclusion

In this post, we explained Depth-First Search (DFS) and implemented the algorithm through a sample problem.
DFS is a very useful tool when solving graph problems, so you will often encounter it in coding tests.
Practice solving various problems to understand and utilize DFS in depth.

Kotlin coding test course, exploring geometry

Geometry is utilized in many fields of computer science and is a frequently tested topic in coding assessments.
In this post, we will address a convex hull problem and explain how to solve it efficiently using Kotlin.

Problem Description

Problem: Given N points in a two-dimensional plane, output the coordinates of the points that form the convex hull in a clockwise manner.

A convex hull refers to the smallest convex polygon that encloses all points on a plane.
This problem is often posed in algorithm challenges, and efficient algorithms include Graham’s scan and Jarvis March.

Input and Output Format

Input: The first line contains an integer N (1 ≤ N ≤ 100,000),
followed by N lines where the x and y coordinates of each point are provided.
(x, y are integers that lie between -10^9 and 10^9.)

Output: Output the coordinates of the points that constitute the convex hull
in a clockwise manner, with each coordinate separated by a space.

Problem-solving Process

To solve the problem, we need to go through the following steps:

  1. Input Data Handling:
    Read the coordinates of the points and store them in a list.
    It is convenient to store each point as a Pair object.
  2. Sorting Coordinates:
    Sort the points based on polar angles.
    Use the leftmost bottom (or top) point as a reference and sort the remaining points based on polar angle.
  3. Generating Convex Hull:
    Use the Graham’s scan algorithm to find the points of the convex hull.
  4. Formatting the Output:
    Organize the results in a clockwise manner and print them in the required format.

Code Implementation

        
            import kotlin.math.atan2
            
            data class Point(val x: Int, val y: Int)
            
            fun convexHull(points: List): List {
                val sortedPoints = points.sortedWith(compareBy({ it.x }, { it.y }))
                val hull = mutableListOf()
                
                // Lower part
                for (point in sortedPoints) {
                    while (hull.size >= 2 && cross(hull[hull.size - 2], hull[hull.size - 1], point) <= 0)
                        hull.removeAt(hull.size - 1)
                    hull.add(point)
                }
                
                val lowerSize = hull.size
                
                // Upper part
                for (i in sortedPoints.size - 1 downTo 0) {
                    val point = sortedPoints[i]
                    while (hull.size > lowerSize && cross(hull[hull.size - 2], hull[hull.size - 1], point) <= 0)
                        hull.removeAt(hull.size - 1)
                    hull.add(point)
                }
                
                hull.removeAt(hull.size - 1) // Remove the last point because it is a duplicate
                
                return hull
            }
            
            fun cross(o: Point, a: Point, b: Point): Int {
                return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
            }
            
            fun main() {
                val n = readLine()!!.toInt()
                val points = List(n) {
                    val (x, y) = readLine()!!.split(" ").map { it.toInt() }
                    Point(x, y)
                }
                val hull = convexHull(points)
                hull.forEach { println("${it.x} ${it.y}") }
            }
        
    

Conclusion

In this post, we explained the problem of finding the convex hull.
Geometric problems are frequently found in actual coding assessments, so it is important
to practice sufficiently and solve various types of problems.
Additionally, enhance your problem-solving skills through other geometric challenges.

Additional Learning Resources

If you want to learn Kotlin more deeply, I recommend the following resources:

Good luck with your coding test preparation and I hope for positive results!

Kotlin coding test course, radix sort

Hello! Today we will discuss Radix Sort, which is essential for job seekers and those preparing for coding tests. Radix sort is one of the most efficient algorithms for sorting integers, particularly shining in large datasets.

What is Radix Sort?

Radix Sort is a type of non-comparative sorting algorithm that processes values by separating them by individual digits. This method is especially effective for integer data and is a stable sort. Unlike most sorting algorithms, Radix Sort is not comparison-based, resulting in a time complexity of O(nk), where n is the number of elements to be sorted, and k is the number of digits in the largest number.

Problem Description

Problem: Sort the given list of integers in ascending order using the Radix Sort algorithm.

You need to output the sorted result using Radix Sort when given the following input:

Input: [170, 45, 75, 90, 802, 24, 2, 66]
Output: [2, 24, 45, 66, 75, 90, 170, 802]

Understanding the Radix Sort Algorithm

The main idea of Radix Sort is to repeatedly use Counting Sort based on each digit. Let’s look at the steps of Radix Sort.

Step 1: Separation of Digits

Each number is processed separately based on its digits (those in the 1s, 10s, 100s place, etc.). When processing each digit, Counting Sort is used to sort these numbers.

Step 2: Counting Sort

Counting Sort is a method of sorting the input list based on a specific digit. Because it is stable, it can maintain the relative order established earlier.

Step 3: Repeat

Starting from the smallest place value, repeat sorting to the largest place value to obtain the final sorted result.

Implementation of Radix Sort (Kotlin Example)

Here is a code implementing Radix Sort using Kotlin:

fun countingSort(array: IntArray, place: Int) {
    val size = array.size
    val output = IntArray(size)
    val count = IntArray(10)

    for (i in 0 until size) {
        count[(array[i] / place) % 10]++
    }

    for (i in 1 until 10) {
        count[i] += count[i - 1]
    }

    for (i in size - 1 downTo 0) {
        output[count[(array[i] / place) % 10] - 1] = array[i]
        count[(array[i] / place) % 10]--
    }

    for (i in 0 until size) {
        array[i] = output[i]
    }
}

fun radixSort(array: IntArray) {
    val max = array.maxOrNull() ?: return

    for (place in 1 until max + 1 step 10) {
        countingSort(array, place)
    }
}

In the code above, the countingSort function is defined to perform Counting Sort first. Then, the radixSort function implements the sorting by iterating over the digits based on the maximum value of the given array.

Code Explanation

  • The countingSort function performs sorting based on the given digit.
  • place indicates the current digit being sorted, starting from the 1s place and increasing by 10.
  • The count array is used to count the occurrences of each digit (0-9).
  • Finally, the sorted result is stored in the output array, and the input array is updated.

Time Complexity of Radix Sort

The time complexity of Radix Sort is O(nk), where k is the number of digits and n is the size of the array. Most other comparison-based sorting algorithms have an average time complexity of O(n log n), making Radix Sort perform much faster, especially when the input data is large with fewer digits.

Limitations of Radix Sort

Radix Sort has several limitations:

  • Suitable for integer data: This algorithm is specialized for integer data and requires a modified version for strings or floating-point numbers.
  • Range Limitation: For integers with large value ranges, memory usage may increase, so caution is needed.
  • Memory Usage: It may require more memory since it generates temporary arrays at intermediate stages.

Conclusion

In this tutorial, we have explored Radix Sort in detail. Radix Sort is a stable and highly effective sorting algorithm in specific situations. Understanding Radix Sort and applying it correctly can be very helpful in coding tests. Next, we will cover other sorting algorithms as well. Thank you!

Kotlin coding test course, greedy algorithm

In this course, we will learn about greedy algorithms using Kotlin, and solve problems that often appear in actual coding tests. A greedy algorithm is a method that finds an overall optimal solution by making the best choice at the moment. This algorithm has properties of optimal substructure and greedy choice. We will delve into the concept of greedy algorithms with specific examples.

1. Understanding Greedy Algorithms

A greedy algorithm is a method that derives the final answer by repeatedly making the ‘best choice at the moment’ to solve a problem. This algorithm might appear simple, but it does not guarantee an optimal solution for all problems. However, for specific problems, using a greedy algorithm can lead to efficient solutions.

1.1 Characteristics of Greedy Algorithms

  • Optimal Substructure: The optimal solution of the problem consists of the optimal solutions of its subproblems.
  • Greedy Choice Property: The choice that seems best at the present is continuously repeated to find the optimal solution.

2. Examples of Problems Using Greedy Algorithms

The problem we will solve this time is the ‘Coin Change Problem’. This problem is a typical example that can be easily solved using greedy algorithms.

Problem: Coin Change

You want to give back the change after purchasing items in a store using the minimum number of coins. The types of coins available and their respective values are as follows.

  • 500 won, 100 won, 50 won, 10 won

For example, if the amount to be given back is 760 won, the change should be given as follows:

  • 500 won: 1 coin
  • 100 won: 2 coins
  • 50 won: 1 coin
  • 10 won: 1 coin

Implement a program that calculates the minimum number of coins required to give the change.

3. Problem Analysis

The reason we approach this problem with a greedy algorithm is that the values of the coins differ. We can use a method that starts with the coin of the highest value and uses it as much as possible to calculate the remaining amount. This allows us to provide the change using the minimum number of coins.

3.1 Algorithm Explanation

We can solve the problem through the following steps:

  1. Input the amount of change to be received.
  2. Starting from the highest value coins, select as many coins as possible.
  3. Repeat this process until the remaining amount is 0.
  4. Output the number of coins used.

4. Kotlin Implementation

Below is an example of implementing the above algorithm in Kotlin.


fun main() {
    // Sort the types of coins in descending order.
    val coins = arrayOf(500, 100, 50, 10)
    // Receive change from user
    val amount = 760 // Set to 760 won as an example.
    
    var remaining = amount
    var coinCount = 0

    for (coin in coins) {
        // Use current coins as much as possible.
        coinCount += remaining / coin
        remaining %= coin
        if (remaining == 0) break
    }

    println("Minimum number of coins to give back: $coinCount")
}
    

4.1 Code Explanation

The code above works as follows:

  • The values of the coins are stored in an array and sorted in descending order.
  • The user inputs the required change.
  • For each coin value, the maximum number is calculated and the remaining amount is updated.
  • Finally, the number of coins used is output.

5. Example Execution and Result

When the above code is executed, you can see the following result:


Minimum number of coins to give back: 5
    

This result indicates that 1 coin of 500 won, 2 coins of 100 won, 1 coin of 50 won, and 1 coin of 10 won were used, totaling 5 coins. This is the result using the minimum number of coins.

6. Additional Problems and Applications

Based on the content of this course, try solving the following modified problems:

  • If the types of coins are given randomly, how can you still find the minimum number of coins?
  • What algorithm will you use if the values of the coins are given in ascending order?

7. Conclusion

We learned that greedy algorithms are a powerful tool for efficient problem-solving. We experienced the implementation of greedy algorithms through the coin change problem and hope to increase our understanding through various application problems.

Additionally, I hope you practice solving more problems to fully master greedy algorithms. Thank you!

kotlin coding test course, representation of graphs

Hello! Today, we will take a detailed look at how to represent graphs, which frequently appear in coding tests, using Kotlin. Graphs are an important data structure in various fields and have essential fundamental functions for solving a range of problems. In this article, we will explain how to represent a graph and how to use that representation to solve problems.

What is a Graph?

A graph is a data structure composed of nodes (vertices) and edges. A node represents an object, and an edge represents the relationship between nodes. Graphs can be classified into directed and undirected graphs based on directionality, and into weighted and unweighted graphs based on the presence of weights.

Ways to Represent a Graph

Graphs can be represented in various ways, with the two most common methods being the Adjacency Matrix and the Adjacency List.

1. Adjacency Matrix

An adjacency matrix is a method of representing a graph using a 2D array of size N x N. N is the number of nodes in the graph, and each element of the matrix indicates whether nodes are connected. For example, if node A is connected to node B, then matrix[A][B] will be 1.

    fun createAdjacencyMatrix(vertices: Int, edges: List>): Array> {
        val matrix = Array(vertices) { Array(vertices) { 0 } }
        for (edge in edges) {
            val (from, to) = edge
            matrix[from][to] = 1
            matrix[to][from] = 1  // For undirected graphs
        }
        return matrix
    }

2. Adjacency List

An adjacency list is a method that uses a list to store connected nodes for each node. This method is memory efficient and advantageous for graphs with fewer nodes or edges.

    fun createAdjacencyList(vertices: Int, edges: List>): List> {
        val list = MutableList(vertices) { mutableListOf() }
        for (edge in edges) {
            val (from, to) = edge
            list[from].add(to)
            list[to].add(from)  // For undirected graphs
        }
        return list
    }

Problem: Friend Relationship Network

The following is a problem that represents a graph of friendships.

Problem: Given N people and their friendships, write a program to check whether a friendship exists between two people. Friend relationships are represented as undirected graphs. The numbers of the two people are given sequentially from 0 to N-1.

Problem-Solving Process

1. Understand the Input Format

First, we need to understand the given data. The input is as follows:

  • The first line contains the number of people N.
  • The second line contains the number of friendships M.
  • Subsequently, M lines contain two integers a and b representing each friendship.

2. Choose a Data Structure

To store the given friendships, we will use an adjacency list. This allows for easy exploration of friendships.

3. Create the Graph

    fun main() {
        val reader = BufferedReader(InputStreamReader(System.`in`))
        val n = reader.readLine().toInt()
        val m = reader.readLine().toInt()

        val edges = mutableListOf>()
        for (i in 0 until m) {
            val (a, b) = reader.readLine().split(" ").map { it.toInt() }
            edges.add(Pair(a, b))
        }

        val adjacencyList = createAdjacencyList(n, edges)
    }

4. Check Friendship

The method to check whether two specific people are friends is to use DFS (Depth First Search) or BFS (Breadth First Search) to verify connectivity. Here, we will implement it using DFS.

    fun isConnected(adjList: List>, start: Int, target: Int, visited: BooleanArray): Boolean {
        if (start == target) return true
        visited[start] = true
        
        for (neighbor in adjList[start]) {
            if (!visited[neighbor]) {
                if (isConnected(adjList, neighbor, target, visited)) {
                    return true
                }
            }
        }
        return false
    }

    fun main() {
        // Continuing from the previous code
        val query = reader.readLine().split(" ").map { it.toInt() }
        val (x, y) = query

        val visited = BooleanArray(n) { false }
        val result = isConnected(adjacencyList, x, y, visited)
        println(if (result) "YES" else "NO")
    }

Conclusion

In this lesson, we learned about the basic concepts and methods of representing graphs, and how to solve the friend relationship network problem using these representations. Graphs are essential for solving various problems, and these graph representation and traversal techniques are very useful in algorithm tests.

Having implemented examples using Kotlin, I encourage you to practice by solving various graph problems yourself. Next time, we will discuss the differences between BFS and DFS, as well as various problem-solving strategies utilizing them. I hope this aids you in your learning journey!

Author: [Your Name]

Date: [Date]