백준 14003번 (가장 긴 증가하는 부분 수열 5, C++)

문제

https://www.acmicpc.net/problem/14003

시간 제한 메모리 제한 제출 정답 맞힌 사람 정답 비율
3 초 512 MB 45574 15974 11322 34.533%

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.

예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이고, 길이는 4이다.

입력

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다.

둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (-1,000,000,000 ≤ Ai ≤ 1,000,000,000)

출력

첫째 줄에 수열 A의 가장 긴 증가하는 부분 수열의 길이를 출력한다.

둘째 줄에는 정답이 될 수 있는 가장 긴 증가하는 부분 수열을 출력한다.

예제 입력 1

plaintext
6
10 20 10 30 20 50

예제 출력 1

plaintext
4
10 20 30 50

한 번의 입력(num)에 의해 생성된 새로운 시퀀스들을 임시로 저장

어떤 수열에도 붙을 수 없으면 독립된 시퀀스 시작

나중에 불필요한 시퀀스(같은 길이인데 꼬리가 큰 것)는 제거

cpp
#include <iostream>
#include <vector>

using namespace std;

int n; // 수열의 개수
vector<vector<int>> Sequences;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    cin >> n;
    if (n <= 0) return 0;

    int num;
    cin >> num;

    // 첫 번째 수로 초기 시퀀스 하나 생성
    vector<int> sequence;
    sequence.push_back(num);
    Sequences.push_back(sequence);

    for (int i = 1; i < n; i++)
    {
        cin >> num;
        int length = Sequences.size();

        bool canExtend = false;
        vector<vector<int>> newSequences;

        // 기존 시퀀스들을 모두 검사
        for (int j = 0; j < length; j++)
        {
            int last = Sequences[j].back();

            // 확장 가능한 경우 : 새로운 시퀀스 생성
            if (last < num)
            {
                canExtend = true;
                vector<int> extendedSeq = Sequences[j];
                extendedSeq.push_back(num);
                newSequences.push_back(extendedSeq);
            }
        }

        // 어떤 시퀀스에도 붙지 못하면 새로운 시퀀스 시작
        if (!canExtend)
        {
            vector<int> newSeq;
            newSeq.push_back(num);
            newSequences.push_back(newSeq);
        }

        // 새로 생긴 시퀀스들을 기존에 합침
        for (auto& seq : newSequences)
            Sequences.push_back(seq);

        // 최적 시퀀스 길이 및 꼬리값 계산
        int maxSeqSize = 0;
        int bestIndex = 0;
        int bestTail = 1000000001;

        for (int j = 0; j < (int)Sequences.size(); j++)
        {
            int sz = Sequences[j].size();
            int tail = Sequences[j].back();
            if (sz > maxSeqSize || (sz == maxSeqSize && tail < bestTail))
            {
                maxSeqSize = sz;
                bestTail = tail;
                bestIndex = j;
            }
        }

        // 가지치기: 같은 길이인데 꼬리값이 큰 시퀀스 제거
        for (int j = 0; j < (int)Sequences.size();)
        {
            int sz = Sequences[j].size();
            int tail = Sequences[j].back();

            if (sz == maxSeqSize && tail > bestTail)
            {
                Sequences.erase(Sequences.begin() + j);
            }
            else
            {
                j++;
            }
        }
    }

	// 결과 중 가장 긴 시퀀스 찾기
    int maxSize = 0;
    int bestIdx = 0;
    for (int i = 0; i < (int)Sequences.size(); i++)
    {
        if (Sequences[i].size() > maxSize)
        {
            maxSize = Sequences[i].size();
            bestIdx = i;
        }
    }

    cout << maxSize << "\n";
    for (int x : Sequences[bestIdx])
        cout << x << " ";

    return 0;
}

브루트포스 방식으로 모든 가능한 증가 수열을 시도하는 방법은 시간 초과 및 메모리 초과로 인하여 통과할 수 없음.

매 반복마다 Sequences의 크기가 기하급수적으로 늘어나는 구조

n개의 원소가 들어오면 2ⁿ에 비례하는 시퀀스로 통과하기 힘들다. (O(2ⁿ))

단계 가능한 Sequences 수
1 [ [1] ]
2 [ [1], [2], [1,2] ]
3 [ [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3] ]
4 거의 모든 가능한 증가 부분 수열이 등장함 (약 15개)

2차 시도 - 이분 탐색 + 인덱스 추적을 이용한 LIS 복원 / O(N log N)

최장 증가 수열 (LIS, Longest Increasing Subsequence)

cpp
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// (LIS, Longest Increasing Subsequence)

int main()
{
    ios::sync_with_stdio(false);
    std::cin.tie(NULL);
    std::cout.tie(NULL);

    int N;
    std::cin >> N;

    vector<int> input(N);
    for (int i = 0; i < N; i++)
        std::cin >> input[i];

    vector<int> tail;  // LIS를 구성하는 인덱스들
    vector<int> prev_idx(N, -1);  //각 요소가 LIS에 포함될 때, 그 직전 요소의 인덱스를 저장


    for (int i = 0; i < N; i++)
    {
        // 이분 탐색으로 input[i]가 tail 배열에서 들어가야 할 위치 찾기
        int lo = 0;
        int hi = (int)tail.size() - 1;
        int pos = (int)tail.size(); // 기본값은 맨 뒤

        while (lo <= hi)
        {
            int mid = (lo + hi) / 2;
            // input[i]보다 크거나 같은 첫 번째 tail 원소 위치(pos)를 찾
            if (input[tail[mid]] < input[i])
            {
                lo = mid + 1;
            }
            else
            {
                pos = mid;
                hi = mid - 1;
            }
        }

        // 이전 인덱스 설정
        if (pos > 0)
        {
            prev_idx[i] = tail[pos - 1];
        }
        else
        {
            prev_idx[i] = -1;  // 첫 번째 요소
        }

        // tail 업데이트
        if (pos == (int)tail.size())
        {
            tail.push_back(i);
        }
        else
        {
            tail[pos] = i;
        }
    }

    // 결과: LIS 길이
    int lis_len = (int)tail.size();
    std::cout << lis_len << "\n";

    // 실제 LIS 수열 재구성
    vector<int> lis;
    for (int idx = tail.back(); idx != -1; idx = prev_idx[idx])
    {
        lis.push_back(input[idx]);
    }
    reverse(lis.begin(), lis.end());

    for (int v : lis)
        std::cout << v << " ";
    std::cout << "\n";

    return 0;
}

input[i]가 들어가야 할 위치(pos)를 찾음

즉, tail 배열 내에서 input[i]보다 작지만 가장 큰 값 다음 위치를 찾는다.

prev_idx[i]는 input[i] 바로 앞에 올 LIS 원소의 인덱스입니다.

pos > 0이면 → 현재 원소가 "길이 pos+1"짜리 LIS의 마지막이 되므로 "그 바로 이전 원소"는 tail[pos - 1]

pos == 0이면 → 수열의 첫 번째 원소이므로 이전이 없음

pos == tail.size() → input[i]가 지금 까지의 모든 수보다 크므로 LIS 길이 1 증가

그렇지 않으면 → 기존 LIS 후보 중 "끝값이 더 큰 것"을 input[i]로 치환 (더 작은 끝값 유지)

예시 1

plaintext
input = [5, 2, 8, 6, 3, 6, 9, 7]
단계 i input[i] tail (인덱스) tail (값으로 표현) prev_idx 설명
0 0 5 [0] [5] [-1, ?, ?, ?, ?, ?, ?, ?] 첫 번째 원소 → 새로운 LIS 시작
1 1 2 [1] [2] [-1, -1, ?, ?, ?, ?, ?, ?] 2 < 5 → 기존 tail[0] 교체 (더 작은 끝값 유지)
2 2 8 [1, 2] [2, 8] [-1, -1, 1, ?, ?, ?, ?, ?] 8 > 2 → 길이 2짜리 LIS 형성
3 3 6 [1, 3] [2, 6] [-1, -1, 1, 1, ?, ?, ?, ?] 6은 2보다 크지만 8보다 작음 → 8을 6으로 교체
4 4 3 [1, 4] [2, 3] [-1, -1, 1, 1, 1, ?, ?, ?] 3은 2보다 크고 6보다 작음 → 6을 3으로 교체
5 5 6 [1, 4, 5] [2, 3, 6] [-1, -1, 1, 1, 1, 4, ?, ?] 6은 3보다 크므로 길이 3짜리 LIS 생성
6 6 9 [1, 4, 5, 6] [2, 3, 6, 9] [-1, -1, 1, 1, 1, 4, 5, ?] 9 > 6 → 길이 4짜리 LIS 생성
7 7 7 [1, 4, 5, 7] [2, 3, 6, 7] [-1, -1, 1, 1, 1, 4, 5, 5] 7은 6보다 크지만 9보다 작음 → 9를 7로 교체

예시 2

plaintext
이분 탐색을 이용한 LIS 구성 로직

input = [3, 5, 2, 7, 4, 1, 8]

--------------------------------

i=0 (값: 3)
tail: [] (빈 배열)
이분탐색: tail.size() = 0 → while문 스킵
pos = 0 (기본값)

prev_idx[0] = -1 (pos=0이므로)
tail 업데이트: pos == tail.size() → push_back(0)
결과: tail = [0] (값: [3])

--------------------------------

i=1 (값: 5)
tail: [0] (값: [3])
이분탐색:
  lo=0, hi=0, mid=0
  input[tail[0]] = 3 < 5 → lo = mid+1 = 1
  lo=1 > hi=0 → 종료
pos = tail.size() = 1

prev_idx[1] = tail[0] = 0 (5 앞에 3)
tail 업데이트: push_back(1)
결과: tail = [0,1] (값: [3,5])

--------------------------------

i=2 (값: 2)
tail: [0,1] (값: [3,5])
이분탐색:
  lo=0, hi=1, mid=0
  input[tail[0]] = 3 >= 2 → pos=0, hi=-1
  lo=0 > hi=-1 → 종료
pos = 0

prev_idx[2] = -1 (pos=0이므로)
tail 업데이트: tail[0] = 2
결과: tail = [2,1] (값: [2,5])

--------------------------------

i=3 (값: 7)
tail: [2,1] (값: [2,5])
이분탐색:
  lo=0, hi=1, mid=0: input[tail[0]]=2 < 7 → lo=1
  lo=1, hi=1, mid=1: input[tail[1]]=5 < 7 → lo=2
  lo=2 > hi=1 → 종료
pos = tail.size() = 2

prev_idx[3] = tail[1] = 1 (7 앞에 5)
tail 업데이트: push_back(3)
결과: tail = [2,1,3] (값: [2,5,7])

--------------------------------

i=4 (값: 4)
tail: [2,1,3] (값: [2,5,7])
이분탐색:
  lo=0, hi=2, mid=1: input[tail[1]]=5 >= 4 → pos=1, hi=0
  lo=0, hi=0, mid=0: input[tail[0]]=2 < 4 → lo=1
  lo=1 > hi=0 → 종료
pos = 1

prev_idx[4] = tail[0] = 2 (4 앞에 2)
tail 업데이트: tail[1] = 4
결과: tail = [2,4,3] (값: [2,4,7])

--------------------------------

i=5 (값: 1)
tail: [2,4,3] (값: [2,4,7])
이분탐색:
  lo=0, hi=2, mid=1: input[tail[1]]=4 >= 1 → pos=1, hi=0
  lo=0, hi=0, mid=0: input[tail[0]]=2 >= 1 → pos=0, hi=-1
  lo=0 > hi=-1 → 종료
pos = 0

prev_idx[5] = -1
tail 업데이트: tail[0] = 5
결과: tail = [5,4,3] (값: [1,4,7])

--------------------------------

i=6 (값: 8)

tail: [5,4,3] (값: [1,4,7])
이분탐색:
  lo=0, hi=2, mid=1: input[tail[1]]=4 < 8 → lo=2
  lo=2, hi=2, mid=2: input[tail[2]]=7 < 8 → lo=3
  lo=3 > hi=2 → 종료
pos = tail.size() = 3

prev_idx[6] = tail[2] = 3 (8 앞에 7)
tail 업데이트: push_back(6)
결과: tail = [5,4,3,6] (값: [1,4,7,8])