최장 증가 수열

주어진 수열에서 오름차순으로 정렬된 가장 긴 부분 수열이다.

예를 들어, 341256784134라는 수열에서 LIS는 345678 or 125678 이 된다.


1. 찾는 방법

LIS의 크기 구하는 방법dp이분탐색에 따라 방법이 나뉘며 경로 추적(trace) 방법은 두 방법 모두 인덱스를 가리키는 배열을 하나 추가하여, 탐색하면서 해당 값의 앞의 수열 인덱스를 저장하는 방법으로 구현한다.

이 배열을 가지고 가장 큰 부분순열을 갖는 값부터 역 추적하며 순열을 구한다.


2. O(n^2)

dp를 이용해 찾는 방법이다.

현재 위치의 수를 끝으로 하는 최장 증가 수열의 값을 dp에 저장하는 방법으로 수열의 첫번째부터 끝까지 반복하며, 현재 위치보다 앞의 수들을 모두 비교하기 때문에 O(n^2) 만큼 걸린다.

1) lis 크기 구하기

void lis_dp(string str) {
    vector<int> dp(str.length(), 0);              //해당 index의 값을 끝으로 하는 가장 긴 수열의 값 저장

    for (int i = 0; i <= str.length(); i++) {
        dp[i] = 1;                                //처음값은 1로 시작
        for (int j = 0; j < i; j++) {
            if (str[i] > str[j] && dp[j] + 1 > dp[i]) {  //현재 위치의 앞의값중 가장 긴 수열의 길이 find
                dp[i] = dp[j] + 1;
            }
        }
    }
    cout << "\nmax length : " << max << endl << "Subsequence : ";
}

2) lis 원소 구하기 (trace)

void lis_dp(string str) {
    vector<int> dp(str.length(), 0);              //해당 index의 값을 끝으로 하는 가장 긴 수열의 값 저장
    vector<int> backtrace_idx(str.length(), -1);  //수열의 이전 값의 index값, -1이 처음 값
    vector<char> lis;                             //최장 증가 수열 값 trace
    int max = 0, idx;

    for (int i = 0; i <= str.length(); i++) {
        dp[i] = 1;  //처음값은 1로 시작
        for (int j = 0; j < i; j++) {
            if (str[i] > str[j] && dp[j] + 1 > dp[i]) {  //현재 위치의 앞의값중 가장 긴 수열의 길이 find
                dp[i] = dp[j] + 1;
                backtrace_idx[i] = j;  // trace 추적 위해
            }
        }
        // trace 추적위해
        if (dp[i] > max) {
            max = dp[i];
            idx = i;
        }
    }

    cout << "\nmax length : " << max << endl << "Subsequence : ";

    /*최장 증가 수열 처음 부분까지 역 추적하며 vector에 추가*/
    for (int i = idx; i >= 0; i = backtrace_idx[i]) {
        lis.push_back(str[i]);
    }

    reverse(lis.begin(), lis.end());  //큰 값부터 추가했으므로 reverse

    for_each(lis.begin(), lis.end(), [](char c) { cout << c << " "; });  // print
}

3. O(nlgn)

이분탐색을 이용하는 방법으로 추가배열에 최장 증가 수열의 값을 저장하는 방법이다.

배열의 인덱스가 수열의 길이를 나타내고, 인덱스의 값은 인덱스만큼의 길이의 수열을 갖는 값을 나타낸다.

추가배열에 마지막값보다 탐색한 현재 값이 더 크다면 배열 마지막에 값을 추가해준다.
(부분수열의 크기를 1증가 시켜주는 것)

탐색한 현재 값이 마지막 값보다 작다면, 추가배열에서 현재값보다 작은값들 중 가장 큰 값을 현재값으로 갱신해준다.
(이때 나오는 현재값보다 작은값들 중 가장 큰 값을 lower bound라고 한다)

추가 배열은 항상 오름차순으로 정렬되어있으므로, 이분 탐색으로 탐색이 가능하기 때문에 O(nlgn)이 된다.


1) lis 크기 구하기

void lis_bs(string str) {
    /*부분 수열 자리수에 해당하는 value값 담기 위한 배열*/
    vector<pair<char, int>> arr;                  // first = value,

    for (int i = 0; i < str.length(); i++) {
        //첫번째 값이거나 현재 값이 arr의  마지막 값보다 크다면 새로 추가 ==> 최장 증가 수열의 크기 1증가
        if (arr.empty() || arr.back().first < str[i]) arr.push_back({str[i], i});

        //현재 값이 arr 마지막 값보다 작다면, lis의 크기 증가는 안된다. lower bound의 값 갱신
        else {
            auto it = lower_bound(arr.begin(), arr.end(), make_pair(str[i], i), cmp);
            *it = {str[i], i};
        }
    }
    cout << "\nmax length : " << arr.size() << endl << "Subsequence : ";
}

2) lis 원소 구하기 (trace)

void lis_bs(string str) {
    /*부분 수열 자리수에 해당하는 value값 담기 위한 배열*/
    vector<pair<char, int>> arr;                  // first = value, second = idx ==> 배열의 길이가 lis 크기다.
    vector<int> backtrace_idx(str.length(), -1);  //수열의 이전 값의 index값, -1이 처음 값
    vector<char> lis;                             //최장 증가 수열 값 trace

    for (int i = 0; i < str.length(); i++) {
        //첫번째 값이거나 현재 값이 arr의  마지막 값보다 크다면 새로 추가 ==> 최장 증가 수열의 크기 1증가
        if (arr.empty() || arr.back().first < str[i]) {
            if (!arr.empty()) backtrace_idx[i] = arr.back().second;  //최장 증가 수열값 trace
            arr.push_back({str[i], i});
        }
        //현재 값이 arr 마지막 값보다 작다면, lis의 크기 증가는 안된다. lower bound의 값 갱신
        else {
            auto it = lower_bound(arr.begin(), arr.end(), make_pair(str[i], i), cmp);
            if (it != arr.begin()) backtrace_idx[i] = (it - 1)->second;  // lis 값 trace ==> lower bound 위치가 맨 앞이라면 수행 x
            *it = {str[i], i};
        }
    }

    cout << "\nmax length : " << arr.size() << endl << "Subsequence : ";

    //*최장 증가 수열 처음 부분까지 역 추적하며 vector에 추가*/
    for (int i = arr.back().second; i >= 0; i = backtrace_idx[i]) {
        lis.push_back(str[i]);
    }
    reverse(lis.begin(), lis.end());                                     //큰 값부터 추가했으므로 reverse

    for_each(lis.begin(), lis.end(), [](char c) { cout << c << " "; });  // print
}

Related Posts

데이터 중심 애플리케이션 설계

데이터 중심 애플리케이션 설계

  • Books
  • 2022년 4월 27일

이 책은 총 3부로 구성되어 1부에서는 근본 개념에 대해 설명하고 2부에서는 데이터를 분산 저장하기 위한 내용을, 3부에서는 한 데이터셋에서 다른 데이터셋을 파생하는 시스템애 대해 설명한다. 1장. 데이터 시스템의 기초 1) 신뢰성 잘못될 수 있는 일을 결함이라 부른다. 이 결함을 예측하고 대처할 수 있는 시스템을 내 결함성/탄력성을 가졌다고 말할 수 있다. 결함과 장애는 동일하지 않은데, 결함은 사양에서 벗어난 시스템의 한 구성 요소로 정의되지만, 장애는 사용자에게 필요한 서비스를 제공하지 못하고 시스템 전체가 멈춘 경우이다....

Read More
연산자

연산자

1. 산술 연산자 구분 연산자 연산 피연산자 타입 사칙 연산과 나머지 + 덧셈 정수, 실수, 복소수, 문자열 - 뺄셈 정수, 실수, 복소수 * 곱셈 정수, 실수, 복소수 / 나눗셈 정수, 실수, 복소수 % 나머지 정수, 실수, 복소수 비트 연산 &amp; AND 비트연산 정수 | OR비트 연산 정수 ^ XOR비트 연산 정수 &amp;^ 비트 클리어 정수 시프트 연산 &laquo; 왼쪽 시프트 정수 &laquo; 양의 정수 &raquo; 오른쪽 시프트 정수 &raquo; 양의 정수 산술 연산은 다른 언어의 연산과 별로 다를 것이 없으며 go는 강타입 언어이기 때문에 반드시 피연산자들끼리의 타입이 같아야만 에러가 발생하지 않는다....

Read More
List를 Array로 Array를 List로 변환

List를 Array로 Array를 List로 변환

  • Java
  • 2021년 5월 27일

List와 Array간의 변환은 기본적으로 for문을 이용하여 하나하나 바꾸어주면 변환이 가능하다. 하지만 for이 아닌 stream API를 이용해서 더 간편하게 바꿀 수 있는 방법을 정리한다. 1. List에서 Array로 변환 1) List -&gt; Object[] List에서 Wrapper객체배열로 바꾸는 것은 List의 toArray() 함수를 이용하면 쉽게 바꿀 수 있고 toArray() 의 매개변수로 변환할 객체배열을 넘겨주면 해당 타입의 배열로 바꾸어 반환된다....

Read More