자바 코딩테스트 강좌, 가장 길게 증가하는 부분 수열 찾기

안녕하세요! 오늘은 자바 코딩 테스트에서 자주 출제되는 문제 중 하나인 ‘가장 길게 증가하는 부분 수열(LIS: Longest Increasing Subsequence)’ 찾기 문제를 다루어보려 합니다. 이 문제는 주어진 수열에서 선택된 원소들의 값이 증가하는 가장 긴 부분 수열의 길이를 찾는 것입니다. 복잡한 알고리즘 문제처럼 보일 수 있지만, 효율적인 접근 방법을 통해 해결할 수 있으며, 이를 통해 코딩 테스트에서 점수를 향상시킬 수 있습니다.

문제 설명

주어진 정수 배열이 있습니다. 이 배열에서 가장 길게 증가하는 부분 수열을 찾아 그 길이를 반환하는 프로그램을 작성하세요. 예를 들어:

입력: [10, 9, 2, 5, 3, 7, 101, 18]
출력: 4
설명: 증가하는 부분 수열은 [2, 3, 7, 101]로 길이가 4입니다.

문제 해결 방법

이 문제를 해결하기 위해 여러 가지 접근 방식을 사용할 수 있습니다. 가장 직관적인 방법 중 하나는 동적 프로그래밍(Dynamic Programming) 기법을 사용하는 것입니다. 이 방법을 사용하면 O(n^2)의 시간복잡도로 문제를 해결할 수 있습니다. 또한, 이 문제를 더욱 최적화하면 O(n log n) 시간복잡도로도 해결할 수 있습니다. 이번 글에서는 두 가지 방법을 모두 다루어보겠습니다.

1. 동적 프로그래밍을 사용한 방법

먼저, 동적 프로그래밍을 사용하여 이 문제를 해결하는 방법을 살펴보겠습니다. 이 방법은 다음과 같은 절차로 진행됩니다:

  1. 길이를 n인 배열이 주어질 때, n 크기의 DP 배열을 생성합니다. DP 배열의 각 원소는 해당 인덱스까지의 가장 긴 증가하는 부분 수열의 길이를 저장합니다.
  2. DP 배열의 초기값으로 모두 1을 설정합니다. (각 원소는 최소한 자기 자신으로만 구성된 부분 수열을 가질 수 있기 때문입니다.)
  3. 이중 루프를 이용하여 각 원소를 확인하고, 이전 원소들과 비교하여 증가하는 부분 수열의 길이를 업데이트합니다.
  4. 최종적으로 DP 배열에서 가장 큰 값을 반환합니다.

이제 이를 자바로 구현해 봅시다.

public class LongestIncreasingSubsequence {

    public static int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int n = nums.length;
        int[] dp = new int[n];
        
        // 모든 수열의 길이를 1로 초기화
        for (int i = 0; i < n; i++) {
            dp[i] = 1;
        }
        
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }
        
        // DP 배열에서 최대값 찾기
        int maxLength = 0;
        for (int length : dp) {
            maxLength = Math.max(maxLength, length);
        }
        
        return maxLength;
    }

    public static void main(String[] args) {
        int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
        System.out.println("가장 길게 증가하는 부분 수열의 길이: " + lengthOfLIS(nums));
    }
}

코드 분석

위의 코드는 다음과 같은 단계로 진행됩니다:

  • 주어진 배열이 null이거나 길이가 0이면 0을 반환합니다.
  • DP 배열을 초기화하여 모든 값이 1이 되도록 설정합니다.
  • 이중 루프를 통해 각 원소를 비교하여 증가하는 부분 수열의 길이를 업데이트합니다.
  • 최종적으로 DP 배열에서 가장 긴 수열의 길이를 찾아 반환합니다.

2. O(n log n) 방법

다음으로, O(n log n) 방법을 살펴보겠습니다. 이 방법은 이진 탐색(Binary Search)을 통해 보다 효율적으로 문제를 해결할 수 있습니다. 이 알고리즘은 다음과 같은 절차를 따릅니다:

  1. 비어있는 리스트를 생성합니다.
  2. 입력 배열의 각 요소를 순회합니다. 리스트의 마지막 원소보다 큰 경우, 해당 원소를 리스트에 추가합니다.
  3. 리스트의 마지막 원소보다 작거나 같은 경우, 이진 탐색을 통해 리스트에서 해당 원소가 들어갈 위치를 찾은 후 그 위치의 원소를 대체합니다.
  4. 리스트의 길이가 결국 가장 길게 증가하는 부분 수열의 길이가 됩니다.

이제 이를 자바로 구현해 보겠습니다.

import java.util.ArrayList;
import java.util.List;

public class LongestIncreasingSubsequenceBinarySearch {

    public static int lengthOfLIS(int[] nums) {
        List lis = new ArrayList<>();
        
        for (int num : nums) {
            int index = binarySearch(lis, num);
            if (index == lis.size()) {
                lis.add(num);
            } else {
                lis.set(index, num);
            }
        }
        
        return lis.size();
    }

    private static int binarySearch(List lis, int num) {
        int left = 0, right = lis.size();

        while (left < right) {
            int mid = left + (right - left) / 2;
            if (lis.get(mid) < num) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        
        return left;
    }

    public static void main(String[] args) {
        int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
        System.out.println("가장 길게 증가하는 부분 수열의 길이: " + lengthOfLIS(nums));
    }
}

코드 분석

위의 코드 분석은 다음과 같습니다:

  • 빈 리스트를 생성하여 가장 길게 증가하는 부분 수열을 저장합니다.
  • 각 원소를 순회하며 이진 탐색을 통해 들어갈 위치를 찾습니다.
  • 리스트의 마지막 원소보다 작은 경우 해당 위치의 원소를 대체합니다.
  • 최종적으로 리스트의 길이를 반환하여 가장 길게 증가하는 부분 수열의 길이를 제공합니다.

결론

오늘은 자바를 사용하여 ‘가장 길게 증가하는 부분 수열’ 문제를 해결하는 두 가지 방법을 알아보았습니다. 동적 프로그래밍을 사용한 O(n^2) 방법과 이진 탐색을 활용한 O(n log n) 방법을 통해 문제를 해결할 수 있었습니다. 이 두 가지 접근법을 통해 코딩 테스트에서 유용하게 활용할 수 있을 것입니다.

여러분이 취업 준비와 코딩 테스트 준비에 도움이 되길 바랍니다. 감사합니다!