C++ Coding Test Course, Binary Tree

A binary tree is one of the important data structures in computer science,
where each node has at most two child nodes.
Binary trees are used for various purposes such as searching, sorting, and data storage,
and frequently appear in algorithm problems.
In this lecture, we will solve algorithm problems utilizing binary trees.

Problem: Finding the Maximum Depth of a Binary Tree

The problem is to traverse a given binary tree and calculate its depth.
The maximum depth refers to the length of the path from the root node to the deepest leaf node.
If the binary tree is empty, the depth is 0.
The following are the input and output formats.

Input Format

The binary tree is given in the form of an array, and each node is defined as follows:


    struct TreeNode {
        int val;
        TreeNode* left;
        TreeNode* right;
        TreeNode(int x) : val(x), left(NULL), right(NULL) {}
    };
    

Output Format

Returns the maximum depth of the binary tree as an integer.

Example


    Input: [3,9,20,null,null,15,7]
    Output: 3
    

Problem Solving Approach

To solve this problem, a method to traverse the binary tree is required.
We can mainly traverse in two ways:
depth-first search (DFS) or breadth-first search (BFS).
In this case, we will choose to use DFS to recursively traverse the tree.

Depth-First Search – Recursive Method

Through recursion, we visit each node of the tree and
continuously call the left and right child nodes to check the depth.
Below is the core idea of the algorithm representing this process.

  1. If the current node is NULL, return 0.
  2. Recursively call the left subtree to obtain its depth.
  3. Recursively call the right subtree to obtain its depth.
  4. Select the greater of the left and right depths and add 1 before returning.

Implementation

Now, let’s implement the above-described algorithm in C++ code.


    class Solution {
    public:
        int maxDepth(TreeNode* root) {
            if (root == NULL) {
                return 0;
            }
            int leftDepth = maxDepth(root->left);
            int rightDepth = maxDepth(root->right);
            return max(leftDepth, rightDepth) + 1;
        }
    };
    

Code Explanation

The above code is a function written in C++ that calculates the maximum depth of a binary tree.
The maxDepth function is called recursively to calculate the
maximum depth of the given node.

  • In if (root == NULL), we check if the node is NULL. If it is NULL, the depth is 0, so we return it.
  • Calculate the depths for the left and right children and choose the larger value before adding 1.
  • The final returned value is the overall maximum depth.

Testing

Now, let’s verify the functionality of the algorithm through some test cases.


    TreeNode* root = new TreeNode(3);
    root->left = new TreeNode(9);
    root->right = new TreeNode(20);
    root->right->left = new TreeNode(15);
    root->right->right = new TreeNode(7);
    
    Solution solution;
    int result = solution.maxDepth(root);
    cout << result; // 3
    

Conclusion

We examined how to implement an algorithm that calculates the maximum depth of
a binary tree in C++.
Through this problem, we were able to learn the basics of recursive traversal,
which will serve as a foundation for solving more complex tree-related problems.
I hope you will continue to study binary trees in depth.
Thank you!

C++ Coding Test Course, Binary Search

Binary Search is an efficient method for finding a specific value in a sorted array. This algorithm has a time complexity of O(log n) because it repeatedly divides the array in half to find the target value. In this lecture, we will cover the basic concepts of binary search, implementation methods, and practical exercises using this technique.

Basic Principle of Binary Search

Binary search operates according to the following procedure:

  1. Check the central element of the array.
  2. Compare the central element with the target value:
    • If the target value is less than the central element, search in the left half of the array.
    • If the target value is greater than the central element, search in the right half of the array.
    • If the target value is equal to the central element, the search is complete.

This process is repeated until the target value is found.

Problem: Finding a Specific Value in a Sorted Array

Given an integer array arr and an integer target, write a program that returns the index of target. If target is not found, it should return -1.

Input Format

  • The first line contains the size of the array n. (1 ≤ n ≤ 105)
  • The second line contains the sorted array arr in ascending order.
  • The third line contains the number target to be found.

Output Format

Print the index of the target. (An integer between 0 and n-1)

Example

Input:
    5
    1 3 5 7 9
    5

    Output:
    2

Solution Process

Step 1: Understanding the Problem

First, it is important to accurately understand the problem. Since we need to quickly find a specific element in a sorted array, we should use binary search.

Step 2: Implementing the Binary Search Algorithm

Let’s implement the binary search algorithm in C++.

#include <iostream>
#include <vector>

using namespace std;

int binarySearch(const vector<int> &arr, int target) {
    int left = 0;
    int right = arr.size() - 1;

    while (left <= right) {
        int mid = left + (right - left) / 2;

        if (arr[mid] == target) {
            return mid;  // Found the target.
        } else if (arr[mid] < target) {
            left = mid + 1;  // Move to the right half.
        } else {
            right = mid - 1;  // Move to the left half.
        }
    }
    return -1;  // Target not found.
}

int main() {
    int n;
    cout << "Enter the size of the array: ";
    cin >> n;
    
    vector<int> arr(n);
    cout << "Enter the elements of the array: ";
    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }
    
    int target;
    cout << "Enter the number you want to find: ";
    cin >> target;

    int result = binarySearch(arr, target);
    if (result != -1) {
        cout << "Index of the target: " << result << endl;
    } else {
        cout << "Target not found." << endl;
    }

    return 0;
}

Step 3: Code Explanation

– The binarySearch function takes an integer vector and a target value as input and returns the index of the target value.
left and right represent the start and end indices of the search interval.
– The while loop runs while left is less than or equal to right.
– In each iteration, the central index mid is computed, and the central element is compared with the target value to determine the next search interval.

Step 4: Testing and Verification

After writing the code, it is essential to run it against various test cases to ensure it returns the correct results. Consider the following tests:

  • When the target value exists in the array
  • When the target value does not exist in the array
  • When the array size is 1
  • When there are duplicate elements

Conclusion

Binary search is a very useful algorithm, especially as it exhibits excellent performance in terms of speed when working with sorted arrays. In this lecture, we understood the basic principles of binary search and implemented actual code. Mastering binary search will greatly help in solving many other algorithm problems.

In the next lecture, we will cover variations of binary search and various applications. I encourage you to enhance your algorithm skills through continuous learning!

© 2023 Algorithm Course | C++ Binary Search

C++ Coding Test Course, Bipartite Graph Checking

Hello! Today we will learn about bipartite graphs. A bipartite graph is a graph in which the set of vertices can be divided into two subsets, and there are no edges between vertices within the same subset. This topic is one of the frequently posed questions in coding tests. In the next session, we will explore the algorithms and coding needed to determine if a graph is bipartite.

Problem Description

The problem is to determine whether the given undirected graph is bipartite. Starting from a vertex, we need to check whether we can visit all vertices while satisfying the bipartite graph conditions.

Problem Example

Given the following graph, determine whether it is a bipartite graph.

Vertices: 6
Edges: 
1 - 2
1 - 3
2 - 4
2 - 5
3 - 6

Looking at the graph above, we can see if it can be divided into two sets. Set A is {1, 4, 5, 6} and Set B is {2, 3}. This graph is indeed a bipartite graph.

Problem Solving Strategy

One method we can use to confirm if a graph is bipartite is Breadth-First Search (BFS) or Depth-First Search (DFS). Both methods can be employed to traverse the graph while coloring each vertex (or grouping them) to verify the bipartite graph conditions.

Algorithm Steps

  1. Represent the graph in the form of an adjacency list.
  2. Run a loop to visit all nodes.
  3. Color the current vertex to divide it into two groups.
  4. Check if adjacent vertices have the same color.
  5. Repeat until all nodes have been visited or until all reachable nodes are visited.

C++ Code Implementation

Let’s take a look at the C++ code to determine if this undirected graph is bipartite.


#include <iostream>
#include <vector>
#include <queue>

using namespace std;

const int MAX = 1000; // Maximum number of vertices
vector<int> graph[MAX];
int color[MAX]; // Color setting: -1 is unvisited, 0 and 1 are groups

bool isBipartite(int start) {
    queue<int> q;
    q.push(start);
    color[start] = 0; // Color the starting vertex

    while (!q.empty()) {
        int v = q.front();
        q.pop();

        for (int neighbor : graph[v]) {
            // If the neighboring vertex has not been visited, color it.
            if (color[neighbor] == -1) {
                color[neighbor] = 1 - color[v]; // Assign a different color than the current vertex
                q.push(neighbor);
            }
            // If the neighboring vertex has the same color as the current vertex, it is not a bipartite graph.
            else if (color[neighbor] == color[v]) {
                return false; // Violates the bipartite graph conditions
            }
        }
    }
    return true;
}

int main() {
    int v, e;
    cout << "Enter the number of vertices and edges: ";
    cin >> v >> e;

    // Initialize the graph
    for (int i = 0; i < MAX; i++) {
        color[i] = -1;
    }

    cout << "Input edges (format: a b): " << endl;
    for (int i = 0; i < e; i++) {
        int a, b;
        cin >> a >> b;
        graph[a].push_back(b);
        graph[b].push_back(a); // Undirected graph
    }

    bool result = true;
    for (int i = 1; i <= v; i++) {
        if (color[i] == -1) {
            // If this vertex is unvisited, check if it is bipartite
            if (!isBipartite(i)) {
                result = false;
                break;
            }
        }
    }

    if (result) {
        cout << "This graph is a bipartite graph." << endl;
    } else {
        cout << "This graph is not a bipartite graph." << endl;
    }

    return 0;
}

Code Explanation

This code takes the vertices and edges of the given graph as input and determines whether the graph is bipartite. The isBipartite function checks if all adjacent vertices are of different colors.

Variable and Structure Explanation

  • graph[MAX]: Adjacency list representation of the graph.
  • color[MAX]: An array representing the color of each vertex.
  • queue<int> q: The queue used for BFS.

Input Example and Output Result

When running the code, an example of inputting edges is as follows.

Enter the number of vertices and edges: 6 5
Input edges (format: a b): 
1 2
1 3
2 4
2 5
3 6

When given the above input, the output will be as follows.

This graph is a bipartite graph.

Conclusion

In conclusion, we learned how to determine if a graph is bipartite using C++. Understanding the basic principle of solving problems using BFS or DFS can be applied to various graph problems. I hope this helps your preparation for coding tests!

Note: This problem has a time complexity of O(V + E) for large graphs, which allows for a reasonably checkable time. I recommend experimenting multiple times with various test cases.

C++ Coding Test Course, Euclidean Algorithm

Hello! Today, we will take a look at one of the algorithms frequently tested in coding interviews, the Euclidean algorithm. In this article, we will conceptually understand what the Euclidean algorithm is and explore the process of solving it step by step using a real problem.

What is the Euclidean Algorithm?

The Euclidean algorithm is an efficient algorithm for finding the greatest common divisor (GCD) of two numbers. This method, first introduced by the ancient Greek mathematician Euclid, is very useful because it can determine the GCD through a finite number of operations on the two numbers.

The basic idea of the Euclidean algorithm is as follows:

  • Given two numbers a and b, let r be the remainder when a is divided by b. That is, a = bq + r (where q is the quotient and r is the remainder).
  • Now GCD(a, b) is the same as GCD(b, r). This means we can continue to find the GCD using the remainder.
  • As we repeat this process, there will come a time when r becomes 0, and the b at that time is the GCD.

Example Problem: Finding the Greatest Common Divisor

Now let’s solve a problem using the Euclidean algorithm to find the greatest common divisor.

Problem Description

Given two natural numbers A and B, write a program to find the greatest common divisor of these two numbers.

Input

  • The first line contains the two natural numbers A and B. (1 ≤ A, B ≤ 1,000,000)

Output

  • Output the greatest common divisor of A and B in the first line.

Example

Input:

24 36

Output:

12

Implementation of the Euclidean Algorithm

Now let’s implement the Euclidean algorithm in C++ to solve the above problem. Below is the C++ code.


#include <iostream>

using namespace std;

// Greatest Common Divisor function
int gcd(int a, int b) {
    while (b != 0) {
        int r = a % b; // Calculate remainder
        a = b; // Assign b to a
        b = r; // Assign remainder to b
    }
    return a; // Return the greatest common divisor
}

int main() {
    int A, B;
    cout << "Enter A and B: ";
    cin >> A >> B; // Get input for A and B
    cout << "The greatest common divisor is: " << gcd(A, B) << endl; // Print the greatest common divisor
    return 0;
}
        

Code Explanation

The structure of the above code is as follows:

  • #include <iostream>: This is the header file for using basic input and output functions in C++.
  • using namespace std;: This syntax allows us to use the standard namespace, letting us omit std::.
  • int gcd(int a, int b): This function calculates the greatest common divisor of the two numbers a and b. It calculates r (the remainder) in a loop until b becomes 0.
  • int main(): This is the entry point of the program, responsible for receiving input from the user and printing the greatest common divisor.

Program Execution Process

When the program is compiled and executed, it prompts the user to enter two natural numbers. For instance, if 24 and 36 are entered, the program proceeds as follows:

  1. Initially, a = 24 and b = 36, so r = 24 % 36 = 24.
  2. Now a changes to 36 and b changes to 24. Again, r = 36 % 24 = 12.
  3. Then a changes to 24 and b changes to 12, with r = 24 % 12 = 0.
  4. Since b has become 0, the value of a, which is 12, is printed as the greatest common divisor.

Time Complexity of the Euclidean Algorithm

The time complexity of the Euclidean algorithm is O(log(min(a, b))). This is because when the sizes of the two numbers are similar, at least one of the numbers is reduced by half during each iteration, making it very efficient.

Modified Problem

Now we can modify the Euclidean algorithm to solve other problems. For example, let’s consider a problem of finding the least common multiple (LCM) of two numbers. The least common multiple can be found using the following formula:

LCM(a, b) = (a * b) / GCD(a, b)

Implementing this formula in C++ would look like this:


#include <iostream>

using namespace std;

// Greatest Common Divisor function
int gcd(int a, int b) {
    while (b != 0) {
        int r = a % b;
        a = b;
        b = r;
    }
    return a;
}

// Least Common Multiple function
int lcm(int a, int b) {
    return (a * b) / gcd(a, b); // Calculate LCM
}

int main() {
    int A, B;
    cout << "Enter A and B: ";
    cin >> A >> B; // Get input for A and B
    cout << "The least common multiple is: " << lcm(A, B) << endl; // Print LCM
    return 0;
}
        

Conclusion

In this article, we learned about finding the greatest common divisor and least common multiple using the Euclidean algorithm. This algorithm is efficient and a simple method that can be applied to various problem-solving scenarios. As it is frequently brought up in coding tests, it is important to practice and become familiar with it. We will be covering more problems and algorithms in the future, so please stay tuned! Thank you.

C++ Coding Test Course, Union Find

The Union-Find data structure is very useful for solving algorithmic problems. In this article, we will explain the concept of Union-Find and detail the problem-solving process using C++.

What is Union-Find?

The Union-Find algorithm is a data structure that efficiently manages a collection of data. This data structure is also known as Disjoint Set and primarily supports the following two operations:

  • Find: This operation finds which set a particular element belongs to. It returns the representative element (root node) of the set.
  • Union: This operation merges two sets. It connects the root nodes of the two sets to form one set.

It is mainly used to find cycles in graph theory or to check connected components. There are two optimization techniques to effectively utilize Union-Find:

1. Path Compression

When performing the Find operation, it updates the parent of all nodes along the path to the root node, allowing for faster retrieval of the root node in subsequent operations.

2. Rank-based Union

When performing the Union operation, it compares the sizes of two sets and connects the smaller set as a child of the larger set. This helps to reduce the height of the tree.

Problem Description

Now, let’s look at a problem that can be solved using the Union-Find algorithm. We will solve the problem below.

Problem: Friend Network

There is a friend network in which several friends are connected to each other. Each friend is identified by a number from 1 to N. Two friends are considered friends if they are directly or indirectly connected. Given a pair of friends (a, b), check whether a and b belong to the same friend group in the friend network.

Input Format

  • The first line contains the number of friends N and the number of friend relationships M. (1 ≤ N ≤ 100,000, 1 ≤ M ≤ 100,000)
  • From the second line onward, M pairs of friend relationships a, b are given.
  • The last line contains the query Q, where each query provides a pair of friends (x, y).

Output Format

For each query, print ‘YES’ if x and y belong to the same friend group, otherwise print ‘NO’.

Algorithm Approach

  1. Initialize and connect the friend relationships using Union-Find. Use Union(a, b) to connect a and b in the same set.
  2. For each query, use Find(x) and Find(y) to check the root nodes of the two friends. If they have the same root node, print ‘YES’; otherwise, print ‘NO’.

C++ Code Implementation

Below is the C++ code that implements the Union-Find algorithm:


#include 
#include 
using namespace std;

class UnionFind {
public:
    UnionFind(int n) {
        parent.resize(n);
        rank.resize(n, 0);
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
        }
    }

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

    void unionSets(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (rootX != rootY) {
            // Rank-based union
            if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;
            } else if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;
            } else {
                parent[rootY] = rootX;
                rank[rootX]++;
            }
        }
    }

private:
    vector parent;
    vector rank;
};

int main() {
    int N, M;
    cin >> N >> M;
    UnionFind uf(N + 1);

    for (int i = 0; i < M; i++) {
        int a, b;
        cin >> a >> b;
        uf.unionSets(a, b);
    }

    int Q;
    cin >> Q;
    for (int i = 0; i < Q; i++) {
        int x, y;
        cin >> x >> y;
        if (uf.find(x) == uf.find(y)) {
            cout << "YES" << endl;
        } else {
            cout << "NO" << endl;
        }
    }

    return 0;
}

Code Explanation

In the above C++ code, we implemented the following components:

  • UnionFind class: Contains the main logic of the Union-Find algorithm. The find function finds the root node, and the unionSets function merges two sets.
  • Main function: Takes the friend relationships as input, stores them in the Union-Find structure, and outputs the results for the given queries.

Time Complexity

The time complexity of the Union-Find algorithm is as follows:

  • Find operation: Almost constant time (α(N), proportional to the inverse Ackermann function)
  • Union operation: Almost constant time

Thus, the overall time complexity of the algorithm is O(M * α(N)), where M is the number of friend relationships.

Conclusion

In this course, we have examined the concept of the Union-Find data structure and the problem-solving process using C++ in detail. Union-Find is a powerful tool that can be effectively utilized in various problems. I hope it will be of great help in your future coding tests.