python coding test course, binary search

1. What is Binary Search?

Binary Search is a highly efficient search algorithm used to find a specific value in a sorted array. This algorithm works by dividing the given list in half to search for the desired value, making it much faster than the typical Linear Search.

The key idea behind binary search is to take advantage of the fact that the list is sorted. The algorithm operates through the following steps:

  1. Find the middle index of the list.
  2. Check if the middle element matches the value you are looking for.
  3. If they do not match, adjust the search range based on the comparison between the middle element and the target value. If it is smaller than the middle element, search the left half; if larger, search the right half.
  4. Repeat this process until the target value is found.

2. Time Complexity of Binary Search

The time complexity of the binary search algorithm is O(log n). This is because it halves the search space at each step when the size of the list to be searched is n. As a result, binary search works efficiently even with very large data sets.

3. Problem: Find the Index of a Specific Value

Problem Description

Given a sorted integer array arr and an integer target, write a binary search function that returns the index of target. If the target does not exist, it should return -1.

Input

  • The first line contains the size of the array n. (1 ≤ n ≤ 10^5)
  • The second line contains n integers separated by spaces.
  • The third line contains the target value target. (-10^9 ≤ target ≤ 10^9)

Output

Print the index of target. A value of -1 indicates that the target does not exist.

4. Example Input for the Problem

                5
                1 2 3 4 5
                3
            

Example Output

2

5. Problem Solving Process

This section describes the necessary steps to solve the problem. We will go through each step to find the target value in the given array using the binary search algorithm.

5.1 Implementation of the Algorithm

First, let’s define the basic structure needed to implement binary search. The function will take the array and the target value as arguments and return the index or -1. Now, let’s write the code.

                def binary_search(arr, target):
                    left, right = 0, len(arr) - 1
                    
                    while left <= right:
                        mid = (left + right) // 2
                        
                        if arr[mid] == target:
                            return mid
                        elif arr[mid] < target:
                            left = mid + 1
                        else:
                            right = mid - 1
                    return -1
            

5.2 Code Explanation

The above code is a simple implementation of the binary search algorithm. left and right indicate the current search range. Initially, left is 0 and right is the last index of the array.

while left <= right: The condition runs while left is less than or equal to right. It calculates the middle value and stores it in mid, and adjusts the range based on comparisons with that value.

5.3 Input Handling and Output

Next, let's add the part that handles input and calls the binary_search function to print the result.

                n = int(input())
                arr = list(map(int, input().split()))
                target = int(input())
                
                result = binary_search(arr, target)
                print(result)
            

6. Code Optimization

While the above code performs basic binary search, there are ways to optimize it further. Particularly in Python, a more convenient method can be used to calculate the middle index of the list. To simplify the process, it is advisable to use Python's integer division instead of directly adding values to calculate mid.

                def binary_search_optimized(arr, target):
                    left, right = 0, len(arr) - 1
                    
                    while left <= right:
                        mid = left + (right - left) // 2
                        if arr[mid] == target:
                            return mid
                        elif arr[mid] < target:
                            left = mid + 1
                        else:
                            right = mid - 1
                    return -1
            

By calculating mid this way, it helps to prevent overflow in Python's memory. This makes the calculation of the middle value safer and more efficient.

7. Variations of the Problem

The binary search algorithm can be applied to various variations beyond finding the index of a specific value. For example, problems include finding the leftmost (or rightmost) index in an array or finding the maximum (or minimum) value that satisfies a specific condition.

7.1 Example Problem: Finding the First Position

Let's solve the problem of finding the first position of a specific value in a given integer array. To do this, we will use binary search, but if the mid value equals the target value, we will continue searching to the left.

                def binary_search_first(arr, target):
                    left, right = 0, len(arr) - 1
                    result = -1  # Variable to store the result
                    
                    while left <= right:
                        mid = left + (right - left) // 2
                        if arr[mid] == target:
                            result = mid     # Store the current index
                            right = mid - 1  # Search left
                        elif arr[mid] < target:
                            left = mid + 1
                        else:
                            right = mid - 1
                    return result
            

8. Conclusion

Binary search is a highly efficient algorithm for finding a specific value in a sorted array. In this tutorial, we covered the basic concepts of binary search, its time complexity, algorithm implementation, optimization, and variations. By using binary search, you can effectively solve problems frequently encountered in coding tests. Continue to practice various problems and master techniques utilizing binary search to enhance your coding skills.

We hope you continue learning various algorithms through more tutorials and problem-solving sessions. Algorithm problems can improve your skills through repetitive practice, so maintain a persistent attitude toward challenges.

python coding test course, determining bipartite graphs

In this article, we will discuss the concept of Bipartite Graphs and the algorithmic problem for determining whether a graph is bipartite.
A bipartite graph is one that can be divided into two sets of nodes, and the key is to determine whether adjacent nodes belong to different sets.

Problem Description

We will solve the problem of determining whether a given graph, made up of vertices and edges, is bipartite.
Specifically, this problem will take the following input.

  • First line: The number of vertices n and the number of edges m are given, separated by a space.
  • Next m lines: Two vertices u and v connected by each edge are provided.

The output will be YES if the given graph is bipartite; otherwise, it will output NO.

Example Input 1

3 3
1 2
2 3
1 3

Example Output 1

NO

Example Input 2

3 2
1 2
2 3

Example Output 2

YES

Problem Solving Process

1. Understanding the Definition of a Bipartite Graph

A bipartite graph is one where, when each node is divided into two groups, no nodes within the same group are connected.
Such graphs can generally be identified through the possibility of bipartite coloring.

In other words, when coloring a node, the adjacent nodes should be colored with the opposite color, and if there are no nodes colored the same until the end, it is a bipartite graph.

2. Graph Representation Method

To represent the given graph, we will use an adjacency list. We maintain a list of vertices connected to each vertex.
In Python, we can easily construct the graph using a dictionary.

Python Code Example (Graph Construction)


def create_graph(n, edges):
    graph = {i: [] for i in range(1, n + 1)}
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)
    return graph

3. Coloring Using BFS or DFS

We can use either the BFS or DFS algorithm to traverse the graph. We will use the BFS method to determine if the graph is bipartite.

The basic idea of BFS is to color the starting node with an arbitrary color and proceed to color all adjacent nodes with the opposite color.
If any adjacent node is already colored and matches the current color we are trying to color it with, then it is not a bipartite graph.

Python Code Example (BFS Coloring)


from collections import deque

def is_bipartite(graph, n):
    color = {}
    for node in range(1, n + 1):
        if node not in color:
            queue = deque([node])
            color[node] = 0  # Color the starting node

            while queue:
                current = queue.popleft()

                for neighbor in graph[current]:
                    if neighbor not in color:
                        color[neighbor] = 1 - color[current]  # Color with opposite color
                        queue.append(neighbor)
                    elif color[neighbor] == color[current]:
                        # If the same color, then it is not a bipartite graph
                        return False
    return True

4. Implementing the Entire Program

Now we will integrate the graph construction and the bipartite determination logic to complete the entire program.


def main():
    n, m = map(int, input().split())
    edges = [tuple(map(int, input().split())) for _ in range(m)]

    graph = create_graph(n, edges)
    
    if is_bipartite(graph, n):
        print("YES")
    else:
        print("NO")

if __name__ == "__main__":
    main()

Conclusion

In this article, we explored the concept of bipartite graphs and the algorithmic problem of determining them.
We explained an efficient method to identify bipartite graphs through BFS and examined a more practical approach using Python code examples.

We plan to cover various algorithm topics in the future, so please continue to stay tuned. Thank you!

Python Coding Test Course, Euclidean Algorithm

Hello. In this blog post, we will take a detailed look at the Euclidean algorithm, which frequently appears in algorithm exams and real employment processes, and solve coding problems utilizing this method.

1. What is the Euclidean Algorithm?

The Euclidean algorithm is an efficient method for finding the greatest common divisor (GCD) of two integers, first proposed by the ancient Greek mathematician Euclid. This method works by repeatedly dividing the two numbers to find their GCD.

Principle of the Euclidean Algorithm

For two given numbers a and b (a > b), GCD(a, b) is equal to GCD(b, a % b). This process is repeated until b becomes 0, and finally, a is the GCD.

Example

For example, let’s find the GCD of 48 and 18.

  1. GCD(48, 18) → GCD(18, 48 % 18) → GCD(18, 12)
  2. GCD(18, 12) → GCD(12, 18 % 12) → GCD(12, 6)
  3. GCD(12, 6) → GCD(6, 12 % 6) → GCD(6, 0)
  4. GCD(6, 0) = 6

2. Problem Definition

Now, let’s define an algorithm problem based on the Euclidean algorithm.

Problem: Write a program that takes two integers as input and outputs their greatest common divisor.

Input: Two integers A and B (0 < A, B < 10^9)

Output: The greatest common divisor GCD(A, B)
    

3. Problem Solving Process

To solve the above problem, we will go through a series of steps.

3.1 Problem Analysis

First, two integers will be given as input. The goal is to find the GCD of these two numbers. It is important to note that since the range of the input numbers is quite large, the algorithm needs to be efficient. The Euclidean algorithm has a time complexity of O(log(min(A, B))) which makes it suitable.

3.2 Algorithm Design

We will use the basic recursive approach of the Euclidean algorithm to find the GCD. Below are the main steps of the algorithm.

  1. Define a function that takes two integers as arguments.
  2. Compare the larger and smaller of the two numbers and compute the remainder when the larger number is divided by the smaller number.
  3. This process is repeated until the remainder becomes 0.
  4. When the remainder is 0, the smaller number at that time is the GCD.

3.3 Python Code Implementation

def gcd(a, b):
    while b != 0:
        a, b = b, a % b
    return a

# Input handling
A, B = map(int, input("Please enter two integers A and B: ").split())
print("The greatest common divisor is:", gcd(A, B))
    

The above code uses the basic Euclidean algorithm to calculate the GCD. It takes two numbers A and B from the user, calls the gcd function, and outputs the result.

4. Complexity Analysis

The time complexity of the Euclidean algorithm is O(log(min(A, B))). This is because the two numbers are halved at each step. This algorithm is very efficient and works quickly even for large numbers.

5. Various Modifications and Applications

The Euclidean algorithm is not just limited to finding GCDs; it can also be applied to solve various other problems. For example:

  • Simplifying fractions: By taking two integers as arguments, you can divide the numerator and the denominator by their GCD to express a complete fraction.
  • Least common multiple: By dividing the product of two numbers by their GCD, you can calculate the least common multiple.

6. Conclusion

In this post, we explored the Euclidean algorithm in detail. It was a great opportunity to study the theory and write actual code through a problem that frequently appears in algorithm exams. I encourage you to use the Euclidean algorithm to solve various problems. Happy Coding!

Now, continue to learn about more Python-related algorithm problems and solutions. Mastering algorithms is an important part of job preparation, and experiencing various problems will be beneficial.

python coding test course, union find

Hello! In this post, we will explore the Union-Find algorithm and solve algorithmic problems using it. The Union-Find is a very useful data structure for managing disjoint sets, providing the ability to quickly determine the connectivity of multiple items and to merge sets.

What is Union-Find?

Union-Find consists of two main functions:

  • Union: An operation that merges the sets containing two elements
  • Find: An operation that finds which set a specific element belongs to

Through these two operations, we can easily manage the relationships between various elements and efficiently solve complex connectivity problems. It is particularly useful in graph problems and network connectivity issues.

How Union-Find Works

Union-Find mainly maximizes efficiency using two techniques:

1. Path Compression

When performing a Find operation, the path is compressed to reduce the height of the tree. This decreases the time complexity when performing multiple Find operations.

2. Union by Rank

While performing the Union operation, we consider the ‘rank’ (height) of the sets, attaching the lower rank set to the higher rank set. This method also reduces the depth of the trees, enhancing operational efficiency.

Problem: Counting the Number of Connected Components

Consider the following problem:

Given n nodes and m edges, find the number of connected components of nodes. The nodes are represented by integers from 0 to n-1, and edge information is given in the form of (a, b).

Input Example

    5 3
    0 1
    1 2
    3 4
    

Output Example

    2
    

Solution Process

To solve this problem, we will use the Union-Find data structure to organize the relationships between each node. We will follow these steps:

Step 1: Initialize the Parent Array

Each node is initialized to have itself as its parent. In other words, we create a parent array and set each node to point to itself.

    parent = [0, 1, 2, 3, 4]  # Initialized according to the number of nodes
    

Step 2: Perform Union Operations

Based on the given edge information, we connect the nodes through Union operations. This combines connected sets into one.

Step 3: Count Connected Sets Using Find Operations

By performing Find operations on each node, we check which set they belong to and count the number of unique root nodes before printing the result.

Implementation

Now, let’s implement the above steps in Python code.

    class UnionFind:
        def __init__(self, size):
            self.parent = list(range(size))
            self.rank = [1] * size
        
        def find(self, p):
            if self.parent[p] != p:
                # Path compression
                self.parent[p] = self.find(self.parent[p])
            return self.parent[p]
        
        def union(self, p, q):
            rootP = self.find(p)
            rootQ = self.find(q)
            
            if rootP != rootQ:
                # Union by rank
                if self.rank[rootP] > self.rank[rootQ]:
                    self.parent[rootQ] = rootP
                elif self.rank[rootP] < self.rank[rootQ]:
                    self.parent[rootP] = rootQ
                else:
                    self.parent[rootQ] = rootP
                    self.rank[rootP] += 1

    # Usage Example
    def count_connected_components(n, edges):
        uf = UnionFind(n)
        
        # Perform Union with edge information
        for u, v in edges:
            uf.union(u, v)
        
        # Count the number of unique roots and return the number of connected components
        root_set = set(uf.find(i) for i in range(n))
        return len(root_set)

    # Input handling
    n = 5
    edges = [(0, 1), (1, 2), (3, 4)]
    print(count_connected_components(n, edges))  # Output: 2
    

Interpreting Results

When executing the above code, it outputs 2. This indicates that there are two sets of nodes that are connected in the given graph.

Time Complexity Analysis

When using Union-Find, the time complexity of each operation approaches O(α(n)), where α is the inverse Ackermann function. Since this function grows extremely slowly, we can consider it to take close to constant time in practical terms.

Conclusion

In this post, we examined the Union-Find data structure and methods for solving problems using it. We confirmed that Union-Find can efficiently solve complex connectivity problems, encouraging you to practice various applications.

Thank you!

python coding test course, topological sort

Topological Sorting is a method of sorting nodes in a Directed Acyclic Graph (DAG). It arranges all edges in such a way that they point from the upper node to the lower node. This sorting is mainly used in determining the order of tasks, dependencies, and various programming problems.

Problem Description

Given the number of classes N and a list of edges indicating the precedence relationships between classes, the problem is to determine the order in which the classes can be taken using the topological sorting algorithm.

Input Format

N = 6
edges = [(2, 1), (3, 1), (4, 1), (6, 4), (5, 2), (5, 3)]

Output Format

1 2 3 4 5 6 (One possible order in which the classes can be taken)

Problem Solving Process

1. Understanding the Problem

The topological sorting problem is to establish the precedence relationships through the given nodes (N) and edge information (edges), and then sort all the nodes. Here, edges is represented in the form of (A, B), indicating that A must be taken before B can be taken.

2. Handling Input Parameters

To implement topological sorting, we first need to construct the graph’s adjacency list and the in-degree array. The in-degree array counts the number of classes each node must attend.

from collections import deque

def topological_sort(N, edges):
    # Initialize graph and in-degree
    graph = [[] for _ in range(N + 1)]
    in_degree = [0] * (N + 1)
    
    # Register edge information in the graph and in-degree
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1

3. Designing the Topological Sorting Algorithm

Now, let’s design the algorithm to perform topological sorting. We will add nodes with an in-degree of 0 to a queue, and as we remove nodes one by one from the queue, we will decrease the in-degrees of the nodes connected to that node. Nodes whose in-degree becomes 0 after the decrease will be added back to the queue. This process will be repeated until the queue is empty. Ultimately, we will return the sorted order of nodes.

    # Add nodes with in-degree of 0 to the queue
    queue = deque()
    for i in range(1, N + 1):
        if in_degree[i] == 0:
            queue.append(i)

    result = []
    
    while queue:
        current = queue.popleft()
        result.append(current)
        
        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    return result

4. Writing and Executing the Complete Code

Now let’s integrate the entire code to write the final code. We can pass the processed input values to the function to check the results.

N = 6
edges = [(2, 1), (3, 1), (4, 1), (6, 4), (5, 2), (5, 3)]

sorted_order = topological_sort(N, edges)
print("Order of classes that can be taken:", sorted_order)

5. Results and Evaluation

Running the above code will allow us to find an order of classes that can be taken through topological sorting. Due to the nature of the graph, multiple correct answers may arise. Therefore, if the algorithm is functioning correctly, it is necessary to broadly validate the validity of the results.

6. Code and Algorithm Optimization

The time complexity of the topological sorting algorithm is O(V + E), where V is the number of vertices and E is the number of edges. This algorithm can operate efficiently even with large datasets, making it a useful tool for employment coding tests.

Conclusion

Topological sorting is a useful algorithm in graph theory, applicable to various problems. In this lecture, we implemented topological sorting using Python and provided content suitable for practical coding tests. We hope you will continue to understand and utilize such algorithmic problems in depth.

References