코틀린 코딩테스트 강좌, 트리의 지름 구하기

안녕하세요. 오늘은 코틀린을 사용하여 트리의 지름을 구하는 방법에 대해 알아보겠습니다.
이 강좌에서는 트리의 기본 개념, 지름의 정의, 알고리즘 접근법에 대해 자세히 설명하고
예제 코드를 통해 실습해 볼 것입니다.

트리와 트리의 지름

트리는 각 노드가 0개 이상의 자식을 가지는 비선형 데이터 구조입니다.
트리는 다음과 같은 특성을 가집니다:

  • 트리는 루트 노드를 가지며, 루트는 다른 노드와 연결됩니다.
  • 자식 노드는 반드시 하나의 부모 노드를 가집니다.
  • 루트에서 잎 노드까지의 경로가 트리의 높이를 정의합니다.

트리의 지름이란 트리내에서 가장 긴 경로의 길이를 의미합니다.
지름은 항상 두 잎 노드 간의 거리로 정의될 수 있습니다.
예를 들어, 아래와 같은 트리를 고려해보겠습니다:

        1
       / \
      2   3
     / \
    4   5
    

이 트리의 지름은 노드 4에서 노드 5로 가는 최장 경로인 4-2-5입니다.
따라서 지름의 길이는 2입니다.

트리의 지름 구하기 알고리즘

지름을 구하기 위해서는 DFS(깊이 우선 탐색) 또는 BFS(너비 우선 탐색)를 사용할 수 있습니다.
일반적으로 DFS가 더 많이 사용되는 방법입니다.
알고리즘의 기본 아이디어는 다음과 같습니다:

  1. 임의의 노드(루트 노드)를 선택하여 첫 번째 DFS를 수행합니다. 이때 가장 멀리 있는 노드를 찾습니다.
  2. 첫 번째 DFS에서 찾은 노드를 시작점으로 하여 두 번째 DFS를 수행합니다.
  3. 두 번째 DFS에서 계산된 최대 거리(지름)를 반환합니다.

코틀린 코드 구현

이제 이 알고리즘을 코틀린으로 구현해보겠습니다.
우리는 노드와 엣지를 표현하기 위해 데이터 클래스를 사용하고,
DFS 메서드를 만들어 트리의 지름을 계산하겠습니다.

데이터 클래스와 그래프 초기화

Kotlin
data class Edge(val to: Int, val weight: Int)

class Tree(val size: Int) {
    private val graph = Array(size) { mutableListOf() }

    fun addEdge(from: Int, to: Int, weight: Int) {
        graph[from].add(Edge(to, weight))
        graph[to].add(Edge(from, weight))
    }

    fun diameter(start: Int): Int {
        // 첫 번째 DFS 수행
        val (farthestNode, _) = dfs(start, BooleanArray(size), 0)
        // 두 번째 DFS 수행
        val (_, diameter) = dfs(farthestNode, BooleanArray(size), 0)
        return diameter
    }

    private fun dfs(node: Int, visited: BooleanArray, distance: Int): Pair {
        visited[node] = true
        var maxDistance = distance
        var farthestNode = node

        for (edge in graph[node]) {
            if (!visited[edge.to]) {
                val (newFarthestNode, newDistance) = dfs(edge.to, visited, distance + edge.weight)
                if (newDistance > maxDistance) {
                    maxDistance = newDistance
                    farthestNode = newFarthestNode
                }
            }
        }

        return Pair(farthestNode, maxDistance)
    }
}

코드 설명

Edge 데이터 클래스: 엣지를 정의하는 클래스입니다.
노드와 가중치를 포함합니다.

Tree: 트리를 표현하는 클래스입니다. 트리의 크기, 그래프 데이터 구조를 초기화합니다.
addEdge 메서드로 엣지를 추가할 수 있습니다.

diameter 메서드: 지름을 계산하는 메서드입니다.
첫 번째 DFS와 두 번째 DFS를 수행합니다.

dfs 메서드: 깊이 우선 탐색을 수행하는 재귀 메서드입니다.
방문한 노드를 체크하고 최대 거리를 계산합니다.
최대 거리를 기준으로 가장 먼 노드와 거리를 반환합니다.

테스트 케이스

트리의 지름을 테스트하기 위해 아래와 같이 엣지를 추가하고 결과를 출력해보겠습니다.

Kotlin
fun main() {
    val tree = Tree(5)
    tree.addEdge(0, 1, 1)
    tree.addEdge(0, 2, 2)
    tree.addEdge(1, 3, 1)
    tree.addEdge(1, 4, 1)

    val diameter = tree.diameter(0)
    println("트리의 지름은: $diameter")
}

실행 결과

    트리의 지름은: 4
    

마무리

이번 강좌에서는 코틀린을 활용하여 트리의 지름을 구하는 방법을 살펴보았습니다.
DFS를 사용한 이 접근 방법은 트리 데이터 구조를 다루는 데 유용하며,
다양한 응용 프로그램에서 활용될 수 있습니다.
트리 관련 문제를 해결하는 데 있어 이 강좌가 도움이 되기를 바랍니다.

앞으로도 다양한 알고리즘과 코딩 테스트 문제를 다룰 예정이니 많은 기대 부탁드립니다!
감사합니다.

코틀린 코딩테스트 강좌, 트리 알아보기

1. 트리(소개)

트리는 계층 구조를 표현하기 위한 자료구조로, 각 노드가 여러 자식을 가질 수 있는 구조입니다. 트리는 노드와 엣지로 이루어져 있으며, 가장 위에 있는 노드를 루트 노드(root node)라 하고, 트리의 끝에 있는 노드는 리프 노드(leaf node)라고 합니다. 트리는 다양한 문제를 해결하고 알고리즘을 구현하는 데 필수적인 개념입니다.

1.1. 트리의 성질

  • n개의 노드를 가진 트리는 항상 n-1개의 엣지를 가진다.
  • 루트 노드는 유일하고, 노드의 부모는 오직 하나이다.
  • 모든 리프 노드는 동일한 깊이에 위치하고 있지는 않다.

2. 알고리즘 문제

문제: 이진 트리의 최대 깊이를 구하라

주어진 이진 트리의 최대 깊이를 계산하는 함수를 작성하시오. 이진 트리의 깊이는 루트 노드에서 가장 깊은 리프 노드까지의 경로에 있는 노드의 수이다.

입력

  • 이진 트리는 노드 유형으로 정의되며, 각 노드는 정수 값을 가진다.
  • 노드는 자식 노드를 가질 수 있으며, 자식 노드는 두 개까지이다.

출력

  • 최대 깊이를 정수로 반환한다.

3. 문제 풀이 과정

3.1. 문제 이해하기

문제를 이해하기 위해 주어진 이진 트리의 특징을 잘 살펴봐야 합니다. 깊이란 루트에서 리프 노드까지 가는 최장 경로를 의미하므로, 탐색을 통해 얻을 수 있습니다. 일반적인 방법은 깊이 우선 탐색(DFS)입니다.

3.2. 트리 노드 클래스 정의하기

우선 트리의 노드를 표현하기 위한 클래스를 정의하겠습니다. 코틀린으로 아래와 같이 구현할 수 있습니다:

data class TreeNode(val value: Int, var left: TreeNode? = null, var right: TreeNode? = null)

3.3. 최대 깊이를 구하는 알고리즘 구현하기

최대 깊이를 재귀적인 방법으로 구할 수 있습니다. 아래의 코드는 DFS를 활용하여 구현한 예제입니다.

fun maxDepth(node: TreeNode?): Int {
        if (node == null) return 0
        val leftDepth = maxDepth(node.left)
        val rightDepth = maxDepth(node.right)
        return Math.max(leftDepth, rightDepth) + 1
    }

위의 함수는 다음과 같이 작동합니다:

  • 트리 노드가 null인 경우, 깊이는 0입니다.
  • 왼쪽과 오른쪽 자식 노드의 깊이를 재귀적으로 호출하고, 둘 중 더 깊은 깊이를 선택합니다.
  • 그리고 1을 더하여 최대 깊이를 반환합니다.

3.4. 전체 예제 구현

최대 깊이를 구하는 전체 코드를 직접 구현해 보겠습니다:

fun main() {
        val root = TreeNode(1)
        root.left = TreeNode(2)
        root.right = TreeNode(3)
        root.left!!.left = TreeNode(4)
        root.left!!.right = TreeNode(5)

        val depth = maxDepth(root)
        println("최대 깊이는: $depth")  // 출력: 최대 깊이는: 3
    }

4. 결론

이진 트리는 알고리즘 문제를 풀기 위한 중요한 주제입니다. 문제를 풀면서 트리의 구조를 이해하고, 재귀적 사고를 활용하는 연습을 통해 코딩테스트에서 유용한 스킬을 익힐 수 있습니다. 앞으로 다양한 트리 관련 문제도 풀어보시길 권장합니다.

5. 추가 연습 문제

  • 이진 검색 트리에서 특정 값을 찾는 문제
  • 이진 트리의 모든 경로를 출력하는 문제
  • 이진 트리의 균형 여부를 확인하는 문제

6. 참고 자료

트리에 대한 이해를 높이기 위해 다음의 자료를 참고하시기 바랍니다:

코틀린 코딩테스트 강좌, 트리 순회하기

오늘의 주제는 트리 순회(Tree Traversal)입니다. 트리는 데이터 구조 중 하나로, 특정한 관계를 가진 요소들이 계층적으로 구성되어 있습니다. 우리가 자주 사용하는 데이터 구조 중 하나인 트리를 이해하고, 기본적인 순회 알고리즘을 구현하는 것은 코딩 테스트에서 매우 유용합니다. 이 글에서는 트리의 기본 개념부터 시작하여, 다양한 순회 방법과 코틀린에서의 구현 방법에 대해 알아보겠습니다.

1. 트리의 기본 개념

트리는 노드(Node)로 구성된 데이터 구조입니다. 각 노드는 값과 자식 노드를 가질 수 있습니다. 트리는 다음과 같은 주요 용어로 설명할 수 있습니다:

  • 루트 노드 (Root Node): 트리의 최상위 노드입니다.
  • 리프 노드 (Leaf Node): 자식 노드가 없는 노드입니다.
  • 내부 노드 (Internal Node): 자식 노드를 가진 노드입니다.
  • 서브트리 (Subtree): 특정 노드를 루트로 하는 트리입니다.

2. 트리 순회의 종류

트리를 순회하는 방법은 크게 세 가지로 나눌 수 있습니다:

  1. 전위 순회 (Pre-order Traversal): 노드 방문 – 왼쪽 서브트리 순회 – 오른쪽 서브트리 순회
  2. 중위 순회 (In-order Traversal): 왼쪽 서브트리 순회 – 노드 방문 – 오른쪽 서브트리 순회
  3. 후위 순회 (Post-order Traversal): 왼쪽 서브트리 순회 – 오른쪽 서브트리 순회 – 노드 방문

3. 문제 설명

다음과 같은 이진 트리가 주어진다고 가정합시다:

                 1
               /   \
              2     3
             / \   / \
            4   5 6   7
    

이 트리에서 전위 순회, 중위 순회 및 후위 순회의 결과를 각각 나열하십시오.

4. 문제 해결 과정

이 문제를 해결하기 위해 가장 먼저 필요로 하는 것은 트리에 대한 클래스 구조를 정의하는 것입니다. 코틀린에서는 다음과 같이 이진 트리의 노드를 클래스 형태로 구현할 수 있습니다:

4.1) 이진 트리 노드 클래스 정의

class TreeNode(val value: Int) {
        var left: TreeNode? = null
        var right: TreeNode? = null
    }

위 클래스는 노드의 값을 저장하는 value와 왼쪽과 오른쪽 자식 노드를 참조하는 left, right 변수를 가집니다.

4.2) 전위 순회 구현

전위 순회는 노드를 방문한 후 자식 노드를 방문하는 방식입니다. 이를 위한 함수는 다음과 같습니다:

fun preOrderTraversal(node: TreeNode?) {
        if (node == null) return
        print("${node.value} ")
        preOrderTraversal(node.left)
        preOrderTraversal(node.right)
    }

4.3) 중위 순회 구현

중위 순회는 왼쪽 자식을 먼저 방문한 후 현재 노드를 방문하는 방식입니다. 다음과 같이 구현할 수 있습니다:

fun inOrderTraversal(node: TreeNode?) {
        if (node == null) return
        inOrderTraversal(node.left)
        print("${node.value} ")
        inOrderTraversal(node.right)
    }

4.4) 후위 순회 구현

후위 순회는 양쪽 자식을 방문한 후 현재 노드를 방문하는 방식입니다. 구현은 다음과 같습니다:

fun postOrderTraversal(node: TreeNode?) {
        if (node == null) return
        postOrderTraversal(node.left)
        postOrderTraversal(node.right)
        print("${node.value} ")
    }

5. 트리 생성 및 테스트

이제 실제로 트리를 생성하고, 각 순회 방법을 테스트해보겠습니다. 다음과 같은 코드로 트리를 구성하고 순회를 실행할 수 있습니다:

fun main() {
        val root = TreeNode(1)
        root.left = TreeNode(2)
        root.right = TreeNode(3)
        root.left?.left = TreeNode(4)
        root.left?.right = TreeNode(5)
        root.right?.left = TreeNode(6)
        root.right?.right = TreeNode(7)

        print("전위 순회: ")
        preOrderTraversal(root)
        println()

        print("중위 순회: ")
        inOrderTraversal(root)
        println()

        print("후위 순회: ")
        postOrderTraversal(root)
        println()
    }

6. 코드 실행 결과

위의 프로그램을 실행하면 다음과 같은 결과를 얻을 수 있습니다:

전위 순회: 1 2 4 5 3 6 7 
중위 순회: 4 2 5 1 6 3 7 
후위 순회: 4 5 2 6 7 3 1 

7. 요약

이번 글에서는 트리에 대한 기본 개념과 트리 순회의 세 가지 방법인 전위 순회, 중위 순회, 그리고 후위 순회에 대해 알아보았습니다. 각각의 순회 방법을 코틀린으로 구현하고, 실전에서도 사용할 수 있는 간단한 예제를 통해 트리 구조를 이해할 수 있었습니다. 코딩 테스트에서 자주 접하게 될 트리 문제이므로, 충분히 연습하여 익숙해지는 것이 중요합니다.

8. 추가 연습 문제

아래의 문제를 풀어보며 트리 순회에 대한 이해도를 높여보세요:

  1. 주어진 이진 트리의 깊이를 계산하는 함수를 작성하시오.
  2. 이진 트리의 최대 경로 합을 찾는 함수를 작성하시오.

9. 결론

이제 트리 구조와 순회 방법에 대한 기본적인 이해를 갖추었으므로, 더 복잡한 코딩문제에도 도전할 준비가 되셨을 것입니다. 트리가 활용되는 다양한 알고리즘 문제들을 풀면서, 이 부분에 대한 실력을 더욱 쌓아보시기 바랍니다. 다음 강좌에서는 그래프 탐색에 대해 다루어 보겠습니다. 감사합니다!

Copyright © 2023 – 코딩테스트 강좌

코틀린 코딩테스트 강좌, 투 포인터

이 글에서는 코틀린을 이용한 알고리즘 문제 해결을 위해 투 포인터 기법을 설명합니다. 투 포인터는 한 배열이나 리스트에서 두 개의 포인터를 이용하여 특정 조건을 만족하는 값이나 범위를 찾는 알고리즘 기법으로, 주로 정렬되어 있는 배열과 관련된 문제에서 많이 사용됩니다.

문제 설명

다음은 투 포인터를 사용하여 해결할 수 있는 문제입니다.

문제: 두 수의 합

주어진 정수 배열 nums와 정수 target가 있을 때, 두 수의 합이 target이 되는 두 수의 인덱스를 리턴하시오. 각 입력에 대해 정확히 하나의 정답이 존재한다고 가정합니다. 같은 요소를 두 번 사용할 수는 없습니다.

예를 들어:

  • 입력: nums = [2, 7, 11, 15], target = 9
  • 출력: [0, 1] (nums[0] + nums[1] = 2 + 7 = 9)

문제 접근 방법

이 문제는 투 포인터 기법을 사용하여 해결할 수 있습니다. 하지만 먼저 배열을 정렬해야 하므로, 원래의 인덱스를 추적하는 것이 필요합니다. 일반적인 투 포인터 접근 방식으로 문제를 해결할 수 있습니다.

1단계: 입력 배열 정렬

배열을 정렬하여 각 포인터가 적절한 위치에서 비교할 수 있도록 합니다. 이 경우, 각 요소와 그 인덱스를 함께 저장할 필요가 있습니다.

2단계: 투 포인터 설정

한 포인터는 배열의 시작에서 시작하고, 다른 포인터는 배열의 끝에서 시작합니다. 두 포인터가 가리키는 값의 합이 target과 같거나 작은 경우 포인터를 이동시킵니다.

3단계: 조건 확인

합이 target보다 크면 오른쪽 포인터를 왼쪽으로 이동시키고, 작으면 왼쪽 포인터를 오른쪽으로 이동시킵니다. 두 포인터가 만날 때까지 이 과정을 반복합니다.

코틀린 코드

아래는 코틀린으로 구현한 코드입니다:


fun twoSum(nums: IntArray, target: Int): IntArray {
    val indexedNums = nums.mapIndexed { index, num -> index to num }.sortedBy { it.second }
    var left = 0
    var right = indexedNums.size - 1

    while (left < right) {
        val sum = indexedNums[left].second + indexedNums[right].second
        when {
            sum == target -> return intArrayOf(indexedNums[left].first, indexedNums[right].first)
            sum < target -> left++
            else -> right--
        }
    }
    throw IllegalArgumentException("No two sum solution")
}

시간 복잡도

이 알고리즘의 시간 복잡도는 O(n log n)입니다. 배열을 정렬하는 데 소요되는 시간이 가장 크기 때문입니다. 그 후 투 포인터 방식으로 변형된 두 수의 합을 찾는데 걸리는 시간은 O(n)입니다.

결론

투 포인터 기술은 매우 유용한 알고리즘 기법으로, 많은 문제들을 효율적으로 해결할 수 있습니다. 이 문제를 통해 투 포인터를 사용하여 배열을 관리하고 값을 찾아내는 방법을 배웠습니다. 연습을 통해 이 기법을 익히고 더 복잡한 문제를 해결할 수 있는 능력을 기르시기 바랍니다.

추가 연습 문제

다음은 추가로 연습할 수 있는 투 포인터 문제들입니다:

  • 주어진 정수 배열에서 합이 특정 값인 모든 쌍을 찾는 문제
  • 정렬된 배열에서 두 포인터를 사용하여 합이 k인 두 수의 쌍의 개수를 찾는 문제
  • 문자열에서 두 포인터를 사용해 회문 여부를 확인하는 문제

위 문제들을 연습하면서 투 포인터 기법에 대한 이해를 더욱 깊이 있게 할 수 있습니다.

코틀린 코딩테스트 강좌, 트라이

코딩테스트에서 자주 등장하는 데이터 구조 중 하나인 트라이는 문자열을 효율적으로 저장하고 검색하는 데
매우 유용합니다. 이번 강좌에서는 트라이의 기본 개념을 이해하고, 코틀린을 이용하여 트라이를 구현하고
특정 문제를 해결하는 과정을 자세히 살펴보겠습니다.

1. 트라이(Trie)란?

트라이는 다중 접두사 문자열 검색을 위해 사용하는 트리 자료 구조입니다.
각 노드는 문자열의 각 문자에 대한 경로를 나타내고, 루트부터 리프까지의 경로는 하나의 문자열을
형성합니다. 따라서, 트라이를 사용하면 문자열의 공통 부분을 공유할 수 있어 메모리 효율성이 뛰어납니다.

예를 들어, “cat”, “cab”, “dog”와 같은 단어들이 있을 때, 이 단어들은 트라이를 통해
다음과 같이 저장될 수 있습니다:

                (root)
                ├── c
                │   ├── a
                │   │   ├── b
                │   │   └── t
                │   └── d
                └── d
                    └── o
                        └── g
            

이처럼 트라이는 문자열 집합의 효율적인 저장 및 검색을 위한 강력한 도구입니다.
특히, 접두사 검색, 단어 완성 기능, 사전 순 정렬 문제에 효과적입니다.

2. 트라이 자료구조의 구현

트라이를 구현하기 위해 두 개의 주요 클래스를 정의할 것입니다:
TrieNodeTrie 클래스입니다.

                class TrieNode {
                    val children: MutableMap = mutableMapOf()
                    var isEndOfWord: Boolean = false
                }
                
                class Trie {
                    private val root: TrieNode = TrieNode()
                    
                    fun insert(word: String) {
                        var node = root
                        for (char in word) {
                            node = node.children.getOrPut(char) { TrieNode() }
                        }
                        node.isEndOfWord = true
                    }
                    
                    fun search(word: String): Boolean {
                        var node = root
                        for (char in word) {
                            node = node.children[char] ?: return false
                        }
                        return node.isEndOfWord
                    }
                }
            

위 코드는 트라이의 기본적인 삽입 및 검색 기능을 제공합니다. TrieNode
클래스는 자식 노드를 저장하고, 단어의 끝을 나타내는 플래그를 담고 있습니다.
Trie 클래스는 삽입과 검색 기능을 제공하며, getOrPut 함수를 사용하여
자식 노드를 동적으로 생성합니다.

3. 문제 설명: 단어 검색

문제: 주어진 단어 리스트와 쿼리 리스트가 있을 때, 각 쿼리가 단어 리스트에 포함되는지
확인하는 프로그램을 작성하시오. 특히, 공통된 접두사를 가지는 단어들을 효율적으로 검색할 수 있도록 합니다.

입력:
– 단어 리스트: [“apple”, “app”, “bat”, “ball”, “batman”]
– 쿼리 리스트: [“app”, “bat”, “banana”, “appl”, “ball”]

출력: 각 쿼리가 단어 리스트에 있는지 여부를
Boolean 값으로 나타내는 리스트를 반환합니다.
예를 들어, [true, true, false, false, true]가 출력되어야 합니다.

4. 문제 해결 과정

위 문제를 해결하기 위해 먼저 단어 리스트를 트라이에 삽입하고,
이후 각 쿼리를 검색하여 결과를 확인하는 방식을 사용할 것입니다.

                fun wordSearch(words: List, queries: List): List {
                    val trie = Trie()
                    for (word in words) {
                        trie.insert(word)
                    }
                    return queries.map { trie.search(it) }
                }
            

wordSearch 함수는 먼저 Trie 객체를 생성하고,
주어진 단어 리스트를 통해 트리에 단어들을 삽입합니다.
이어서 쿼리 리스트를 순회하며 search 메서드를 호출하여
각 쿼리가 존재하는지 여부를 확인합니다.

5. 코드 실행 예제

                fun main() {
                    val words = listOf("apple", "app", "bat", "ball", "batman")
                    val queries = listOf("app", "bat", "banana", "appl", "ball")
                    val result = wordSearch(words, queries)
                    println(result) // [true, true, false, false, true]
                }
            

위의 코드 블록을 실행하면 쿼리에 대한 답을
Boolean 값으로 포함한 리스트가 출력됩니다. 이처럼 트라이는
문자열 검색 문제를 간단하고 효율적으로 해결해줍니다.

6. 트라이의 유용성과 확장

트라이는 문자열 검색 외에도 여러 가지 다른 문제에 응용될 수 있습니다.
예를 들어, 자동 완성 기능, 접두사 검색 기능은 트라이의
가장 일반적인 활용 사례입니다. 또한, 트라이를 통해 문제를
확장하여 추가 기능을 넣을 수도 있습니다. 특정 접두사로 시작하는
모든 단어를 찾는 기능이나 특정 패턴으로 시작하는 단어를 찾는
기능을 구현할 수 있습니다.

예를 들어, 특정 접두사로 시작하는 모든 단어를 찾는
startsWith 메서드를 다음과 같이 추가할 수 있습니다.

                fun startsWith(prefix: String): Boolean {
                    var node = root
                    for (char in prefix) {
                        node = node.children[char] ?: return false
                    }
                    return true
                }
            

이 메서드는 주어진 접두사가 트리에 존재하는지 여부를
확인할 수 있습니다. 이러한 확장 기능은 특히 대용량 데이터
처리 시 매우 유용합니다.

7. 결론

지금까지 코틀린을 사용하여 트라이를 구현하고, 문자열 검색
문제를 해결하는 과정을 살펴보았습니다. 트라이는 문자열을
효율적으로 관리하고 검색하는 데 매우 유용한 자료구조로,
코딩테스트에서의 문제가 많습니다. 문제를 해결하기 위한
다양한 접근 방식을 시도하고, 트라이를 적절히 활용한다면
다양한 알고리즘 문제를 쉽게 해결할 수 있을 것입니다.

다음 강좌에서는 트라이의 변형 및 다른 고급 문자열 알고리즘에 대해
다룰 예정입니다. 더 많은 연습 문제와 실제 코딩테스트 문제를 통해
실력을 쌓아가시기 바랍니다. 감사합니다!