Computer Science/Computer Algorithms

[Computer Algorithms] LIS (Longest Increasing Subsequence, 최장 증가 부분 수열)

__K.Jun__ 2024. 8. 3. 01:45

LIS (Longest Increasing Subsequence)

말 그대로 가장 긴 증가하는 부분 수열이다. 수열에서 LIS를 구하는 방법은 두 가지가 있다.

1. DP를 이용한 방법 -> 시간 복잡도 O(N^2)

2. 이분 탐색을 이용한 방법 -> 시간 복잡도 O(N * logN)

 

Finding LIS using DP

- LIS의 길이 구하기 (DP)

문제 링크 : https://www.acmicpc.net/problem/11053

#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
#include <map>
#include <algorithm>
#define endl '\n'
using namespace std;

int N;
vector<int> A, DP;
int maxVal = 0;

void input()
{
    cin >> N;
    A.resize(N);
    DP.resize(N);

    for (int i = 0; i < N; i++)
    {
        cin >> A[i];
    }
}

void solution()
{
    for (int i = 0; i < N; i++)
    {
        DP[i] = 1;
        for (int j = 0; j < i; j++)
        {
            if (A[j] < A[i] && DP[i] < DP[j] + 1)
                DP[i] = DP[j] + 1;
        }
    }

    for (int i = 0; i < N; i++)
    {
        if (maxVal < DP[i])
            maxVal = DP[i];
    }
}

void output()
{
    cout << maxVal << endl;
}

int main(void)
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    input();
    solution();
    output();
    return 0;
}

DP[i]는 0~i 수열의 LIS의 길이를 저장한다.

1. DP[i]를 1로 설정한다.

2. j를 0 부터 i - 1 이라고 할 때, A[j]중 A[i]보다 작은 값이 있다면 DP[i] = DP[j] + 1로 업데이트 해준다.

 

- LIS와 LIS의 길이 구하기 (DP)

문제 링크 : https://www.acmicpc.net/problem/14002

#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
#include <map>
#include <algorithm>
#define endl '\n'
using namespace std;

int N;
vector<int> A, DP, parent, LIS;
int maxVal = 0, maxIndex = 0;

void input()
{
    cin >> N;
    A.resize(N);
    DP.resize(N);
    parent.resize(N, -1);

    for (int i = 0; i < N; i++)
    {
        cin >> A[i];
    }
}

void solution()
{
    for (int i = 0; i < N; i++)
    {
        DP[i] = 1;
        for (int j = 0; j < i; j++)
        {
            if (A[j] < A[i] && DP[i] < DP[j] + 1)
            {
                DP[i] = DP[j] + 1;
                parent[i] = j;
            }
        }
    }

    for (int i = 0; i < N; i++)
    {
        if (maxVal < DP[i])
        {
            maxVal = DP[i];
            maxIndex = i;
        }
    }

    for (int index = maxIndex; index != -1; index = parent[index])
    {
        LIS.push_back(A[index]);
    }
    reverse(LIS.begin(), LIS.end());
}

void output()
{
    cout << LIS.size() << endl;
    for (int e : LIS)
        cout << e << " ";
    cout << endl;
}

int main(void)
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    input();
    solution();
    output();
    return 0;
}

1. 모든 노드의 parent의 인덱스를 -1로 초기화한다.

2. j를 0 부터 i - 1 이라고 할 때, A[j]중 A[i]보다 작은 값이 있다면 i의 parent는 j가 된다.

3. DP의 값이 가장 큰 원소의 인덱스를 maxIndex에 저장한다.

4. parent의 인덱스가 -1이 될 때까지 트랙킹 하며 LIS를 채운다.

 

Finding LIS using Binary Search

- LIS의 길이 구하기 (Binary Search)

문제 링크 : https://www.acmicpc.net/problem/12738

#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
#include <map>
#include <algorithm>
#define endl '\n'
using namespace std;

int N;
vector<int> A, LIS;

void input()
{
    cin >> N;
    A.resize(N);
    for (int i = 0; i < N; i++)
    {
        cin >> A[i];
    }
}

void solution()
{
    for (int i = 0; i < N; i++)
    {
        auto pos = lower_bound(LIS.begin(), LIS.end(), A[i]);
        if (pos == LIS.end())
            LIS.push_back(A[i]);
        else
            *pos = A[i];
    }
}

void output()
{
    cout << LIS.size() << endl;
}

int main(void)
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    input();
    solution();
    output();
    return 0;
}

  A를 순차적으로 순회하며 LIS 배열에 A[i]보다 크거나 같은 값이 없다면, 즉 A[i]가 LIS에 현재 있는 모든 값보다 크다면 LIS에 들어간다. 

LIS에 A[i]보다 크거나 같은 값이 있다면, 새로운 LIS를 업데이트 하기 위해서 그 자리를 A[i]로 대체한다. 따라서 LIS의 길이는 정확하겠지만 최종적으로 LIS의 들어있는 원소들은 실제 수열에서의 LIS가 아닐 수 있다.

 

- LIS와 LIS의 길이 구하기 (Binary Search)

문제 링크 : https://www.acmicpc.net/problem/14003

#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
#include <map>
#include <algorithm>
#define endl '\n'
using namespace std;

int N, lisIndex;
vector<int> A, LIS, parent, LISIndices;

void input()
{
    cin >> N;
    A.resize(N);
    parent.resize(N, -1);
    for (int i = 0; i < N; i++)
    {
        cin >> A[i];
    }
}

void solution()
{
    for (int i = 0; i < N; i++)
    {
        auto pos = lower_bound(LIS.begin(), LIS.end(), A[i]);
        lisIndex = pos - LIS.begin();

        if (pos == LIS.end())
        {
            LIS.push_back(A[i]);
            LISIndices.push_back(i);
        }
        else
        {
            *pos = A[i];
            LISIndices[lisIndex] = i;
        }

        if (lisIndex > 0)
            parent[i] = LISIndices[lisIndex - 1];
    }

    int index = LIS.size() > 0 ? LISIndices.back() : -1;

    LIS.resize(0);
    while (index != -1)
    {
        LIS.push_back(A[index]);
        index = parent[index];
    }
    reverse(LIS.begin(), LIS.end());
}

void output()
{
    cout << LIS.size() << endl;
    for (int e : LIS)
        cout << e << " ";
    cout << endl;
}

int main(void)
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    input();
    solution();
    output();
    return 0;
}

LIS에 들어있는 원소가 A에서 몇번째 원소인지 저장하는 LISIndices 배열을 추가한다.

1. 모든 노드의 parent의 인덱스를 -1로 초기화한다.

2. lower_bound함수를 통해서 i번째 수가 LIS에 삽입될 위치를 lisIndex에 저장한다. 이 위치의 인덱스가 0보다 크다면, i번째 수의 parent는 LISIndices[lisIndex - 1]이다.

3. LIS.size()가 0보다 크다면, index에 LISIndices의 맨 뒤에 있는 값을 저장한다.

4. parent의 인덱스가 -1이 될 때까지 트랙킹 하며 LIS를 채운다.

728x90