문제풀이

[백준/BOJ] 17411 가장 긴 증가하는 부분 수열 6 (c++)

소파와 바보개 2025. 3. 4. 21:49
728x90

[백준/BOJ] 17411 가장 긴 증가하는 부분 수열 6 (c++)

17411번: 가장 긴 증가하는 부분 수열 6

14. Longest Increasing Subsequence (LIS, c++)

 

14. Longest Increasing Subsequence (LIS, c++)

Longest Increasing Subsequence(LIS)? 가장 긴 증가하는 부분수열 혹은 가장 긴 감소하는 부분 수열을 찾는 것을 응용한 문제들이 있습니다. 이 문제는 최대 길이의 증가 또는 감소하는 부분 수열을 찾아

dnsldprp.tistory.com


이번 문제는 LIS의 길이와 구성요소를 하나만 출력하는 것이 아니라, LIS의 길이와 갯수를 찾아야 합니다.

 

2025.03.04 - [문제풀이] - [백준/BOJ] 14003 가장 긴 증가하는 부분 수열 5 (c++)

 

[백준/BOJ] 14003 가장 긴 증가하는 부분 수열 5 (c++)

[백준/BOJ] 14003 가장 긴 증가하는 부분 수열 5 (c++) 14003번: 가장 긴 증가하는 부분 수열 5 14. Longest Increasing Subsequence (LIS, c++)  14. Longest Increasing Subsequence (LIS, c++)Longest Increasing Subsequence(LIS)? 

dnsldprp.tistory.com

 

위 문제와 동일하게 d에는 LIS의 k번째 위치할 수 있는 가장 작은 숫자를 저장하고 p에는 i번째 숫자가 LIS의 k번째에 위치할 수 있었다는 정보를 저장합니다. a에는 실제 수열의 숫자들이 저장되어 있습니다.

 

p에 담긴 정보를 이용하면 LIS의 갯수를 세어볼 수 있습니다.

 

i번째 숫자까지 만들 수 있는 LIS의 갯수는 모든 i-1번째 숫자에 대해서

 

a[i] > a[i-1] 이면서 p[i] = p[i-1]+1 인 수들의 경우의 수의 합으로 계산해볼 수 있습니다.

 

그러나 이렇게 모든 i-1 번째까지 탐색을 하게되면 시간초과가 발생을 할 것입니다.

 

어딘가 정렬이 되어있어야 탐색을 빠르게 할 수 있을텐데,

 

반대로 생각을 해보면 LIS를 구하는 과정에서 더 큰 숫자가 나오면 LIS의 length가 늘어나기 때문에 p의 값인 k가 같은 숫자들은 역방향으로 탐색할 시 항상 오름차순으로 나오게 됩니다.

 

{ 2, 1, 5, 0, 7, 4, 8, 3 }  이라는 수열에 대해 계산해보면 아래와 같습니다.

 

P 가 0인 경우 -> { 0, 1, 2 }

P 가 1인 경우 -> { 4, 5 }

  P D
2 0 { 2 }
1 0 { 1 }
5 1 { 1, 5 }
0 0 { 0, 5 }
7 2 { 0, 5, 7 }
4 1 { 0, 4, 7 }
8 3 { 0, 4, 7, 8 }
3 1 { 0, 3, 7, 8 }

 

그러므로 정리해보면 P[i] = LIS의 길이인 숫자들은 항상 1개 만들 수 있습니다.

 

P[i]가 LIS길이보다 작은 숫자들은, P[i]+1 을 가진 수들 z에 대해서

1) z는 i보다 나중에 등장해야 하고

2) a[i] < a[z] 인

3) 모든 z들의 경우의 수의 합으로 계산할 수 있습니다.

 

1) z는 i보다 나중에 등장해야 한다. 는 뒤에서 부터 순차적으로 탐색을 하면 해결이 됩니다.

 

2) a[i] < a[z] 는 P의 값인 k가 같은 숫자들은 오름차순으로 등장하기 때문에 k에 따라 다른 vector를 사용하면 k+1의 vector에 upper_bound를 검색하는 것으로 O(logN)에 구할 수 있습니다.

 

3) 모든 z들의 경우의 수의 합은 k에 따라 다른 vector에 prefix sum을 구해두면 원하는 범위에 대한 합을 O(1)에 구할 수 있습니다. 편의를 위해 모든 psum vector에는 0을 미리 추가해 두는 것이 좋습니다.

 

반대로 탐색시 값이 구해지는 과정은 다음과 같습니다.

  r upper_bound psum (전체 합 - upper_bound index) 
= upper_bound 숫자까지의 경우의 수
3 r[1] = { 3 } r[2]에 수행 = 0 psum[2]에 대해 수행 (0-0)=0, psum[1] = { 0, 0 }
8 r[3] = { 8 } - psum[3] = { 0, 1 }
4 r[1] = { 3, 4 } r[2]에 수행 = 0 psum[2]에 대해 수행 (0-0)=0, psum[1] = { 0, 0, 0 }
7 r[2] = { 7 } r[3]에 수행 = 0 psum[3]에 대해 수행 (1-0)=1, psum[2] = { 0, 1 }
0 r[0] = { 0 }  r[1]에 수행 = 0 psum[1]에 대해 수행 (0-0)=0, psum[0] = { 0, 0 }
5 r[1] = { 3, 4, 5 } r[2]에 수행 = 0 psum[2]에 대해 수행 (1-0)=0 psum[1] = { 0, 0, 0, 1 }
1 r[0] = { 0, 1 } r[1]에 수행 = 0 psum[1]에 대해 수행 (1-0)=0 psum[0] = { 0, 0, 1 }
2 r[1] = { 0, 1, 2 } r[1]에 수행 = 0 psum[1]에 대해 수행 (1-0)=0 psum[0] = { 0, 0, 1, 2 }

 

이와 같은 방식으로 LIS의 총 길이는 4이며, 길이 4인 LIS는 2개 존재함을 찾아낼 수 있습니다.

 

이 문제에서는 psum간의 -연산이 있으므로 MOD 연산 사용시 역전되는 것을 주의해야합니다. 따라서 long long을 사용하고 결과에 영향을 주지 않는 선에서 MOD연산을 해주어야 합니다.


#include <bits/stdc++.h>
using namespace std;
#define REP(i, s, e) for (int i=s; i<e; i++)
#define MOD 1000000007
int N, x;
vector<int> a, d, p;
vector<vector<int>> r(1000000);
vector<vector<long long>> psum(1000000);

int solve() {
    REP(i, 0, N) {
        int index = lower_bound(d.begin(), d.end(), a[i])-d.begin();
        if (index >= d.size()) d.push_back(a[i]);
        else d[index] = a[i];
        p.push_back(index);
    }
    REP(i, 0, d.size()) {
        psum[i].push_back(0);
    }
    REP(i, 0, N) {
        int len = p[N-i-1];
        if (len == d.size()-1) {
            psum[len].push_back(psum[len].back()+1);
        } else {
            int index = upper_bound(r[len+1].begin(), r[len+1].end(), a[N-i-1])-r[len+1].begin();
            long long cnt = (psum[len+1].back()-psum[len+1][index])%MOD;
            psum[len].push_back(psum[len].back()+cnt);
        }
        r[len].push_back(a[N-i-1]);
    }

    cout << d.size() << " " << (psum[0].back()%MOD);
    return 0;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> N;
    REP(i, 0, N) {
        cin >> x;
        a.push_back(x);
    }
    solve();
    return 0;
}
728x90