[BOJ] 22971 증가하는 부분 수열의 개수

최장 증가 부분 수열(LIS)의 갯수를 구하는 방법을 알아봅시다

0. 들어가기

1. 문제 요약

길이가 NNN인 어떤 수열 A1A_1A1, A2A_2A2, ... ANA_NAN이 주어진다.

A1A_1A1에서 끝나는 증가하는 부분 수열의 갯수, A2A_2A2로 끝나는 증가하는 부분 수열의 갯수, ... ANA_NAN으로 끝나는 증가하는 부분 수열의 갯수를 각각 998,244,353으로 나눈 나머지를 공백으로 구분하여 출력한다.

증가하는 부분 수열이란, 원래 수열에서 1개 이상의 원소를 순서대로 선택하면서, 맨 처음 원소를 제외한 원소들은 바로 전 원소보다 커야 한다.

참고로, 길이가 1인 부분 수열은 증가하는 부분 수열이다.

1.1. 입력

NNN
A1A_1A1 A2A_2A2 ... ANA_NAN

  • NNN : 수열의 길이, 1N5,0001 \le N \le 5,0001N5,000
  • AiA_iAi : 수열의 원소들, 1Ai5,0001 \le A_i \le 5,0001Ai5,000

2. 풀이 정리

dp[i]AiA_iAi에서 끝나는 부분 수열의 개수를 저장한다.

  • dp[1]A1A_1A1에서 끝나는 부분 수열의 갯수이다.
    • A1A_1A1로 증가하는 부분 수열을 만들 수 있으므로, dp[1]은 1이다.
  • dp[2]A2A_2A2에서 끝나는 부분 수열의 갯수이다.
    • 우선 A2A_2A2만으로 증가하는 부분 수열을 만들 수 있으므로, dp[2]에 1을 저장한다.
    • 만약 A1<A2A_1 \lt A_2A1<A2이라면, A1,A2A_1, A_2A1,A2라는 증가하는 부분 수열을 만들 수 있다. 이 경우, dp[2] += dp[1]`이 된다.
  • dp[3]A3A_3A3에서 끝나는 부분 수열의 갯수이다.
    • A3A_3A3만으로 증가하는 부분 수열을 만들 수 있으므로, dp[3]에 1을 저장한다.
    • 만약 A1<A3A_1 \lt A_3A1<A3이라면, A1,A3A_1, A_3A1,A3라는 증가하는 부분 수열을 만들 수 있다. 이 경우, dp[3] += dp[1]이 된다.
    • 만약 A2<A3A_2 \lt A_3A2<A3이라면, A2A_2A2로 끝나는 모든 증가하는 부분 수열 마지막에 A3A_3A3을 붙여 새로운 증가하는 부분 수열들을 만들 수 있다. 이 경우, dp[3] += dp[2]가 된다.
  • dp[i]AiA_iAi에서 끝나는 부분 수열의 갯수이다.
    • AiA_iAi만으로 증가하는 부분 수열을 만들 수 있으므로, dp[i]에 1을 저장한다.
    • A1A_1A1 ~ Ai1A_{i - 1}Ai1AiA_iAi를 비교해서, AiA_iAi가 큰 경우, 마지막에 AiA_iAi를 추가한 새로운 증가하는 부분 수열을 만들 수 있다.
    • for j in (0..i-1): if A_j < A_i: dp[i] += dp[j]

 

출력은 998,244,353으로 나눈 나머지를 출력해야 하는데, dp[i]를 모두 구하고 나서 나머지를 구하면 dp[1] ~ dp[j]를 더하는 과정에서 오버플로우가 발생할 수 있다. 더하기 연산을 한 번 하면 나머지 연산을 바로 해줘서 오버플로우를 방지하자.

3. 구현

실제 구현은 0번 인덱스부터 사용하도록 했다.

#include <iostream> using namespace std; int N, A[5005]; int dp[5005]; int M = 998'244'353; int main() { cin >> N; for (int i = 0; i < N; i++) { cin >> A[i]; dp[i] = 1; for (int j = 0; j < i; j++) { if (A[j] >= A[i]) continue; dp[i] += dp[j]; dp[i] %= M; } cout << dp[i] << " "; } return 0; }