Jgtony Developer blog

알고리즘 - 다이나믹 프로그래밍

알고리즘 문제에서 흔히들 동적계획법 으로 알려져 있는 다이나믹 프로그래밍에 대한 개념설명과 기초 문제 풀이이다. 매우 기초를 벗어난 모든 알고리즘 문제는 이 접근법을 필수적으로 사용하는 경우가 많기 때문에 반드시 숙지해야 하는 알고리즘이다.

다이나믹 프로그래밍

큰 문제를 작은 문제로 나눠서 푸는 알고리즘이다

다이나믹 프로그래밍의 속성

  1. 작은 문제(부분 문제)가 모두 겹친다
  2. 최적 부분 구조이다. 작은(부분) 문제의 답이 결과 값을 구할때의 과정에 사용된 값과 항상 같다.

예를들어 피보나치 문제의 경우에는 작은 문제 두개의 합으로 큰 문제를 구한다. 또한 계속해서 나타나는 작은 문제들의 값이 중복적으로 같다.

  • 다이나믹 프로그래밍에서 각 문제는 한번만 푼다
  • 최적 부분구조를 만족하므로, 같은 문제는 항상 답이 같다
  • 정답을 구했으면, 어딘가에 저장(메모)한다
  • 메모는 배열에 저장하는것으로 할 수 있다
  • 메모를 하는 것이라서 Memorization 이라고 한다



피보나치 수 예제

int memo[100];
int fibonacci(int n) {
    if(n<=1) return n;
    else {
        if(memo[n]>0) return memo[n];
        memo[n] = fibonacci(n-1) + fibonacci(n-2);
        return memo[n];
    }
}

위 코드의 시간복잡도는 다음과 같이 계산할 수 있다.

기억해야 할 점은 모든 문제를 한번씩 푼다는 것이다.

함수 호출은 n번 일어난다. 여기에 함수의 시간복잡도를 계산해주면 된다. 함수의 시간복잡도는 O(1) 이다.

따라서 위 코드의 시간복잡도는 O(n) 이다.



다이나믹 프로그래밍 구현 방법

  1. Top-down : 재귀함수를 이용한 방법 (모든 문제를 쪼개놓고 필요할 때 구한다)
  2. Bottom-up : 작은문제부터 풀고, 조금씩 크게 만들면서 문제를 푼다 (모든 문제를 쪼개놓고 풀어놓는다)

위 방법중 아무거나 쓰면 된다. 정답이 없다.




문제 풀이 전략

맨 처음 먼저 점화식을 세운다!!

이 문제를 구하기 위해서 어떤 작은 문제들이 필요한가??

작은 문제들이 어떻게 원래 문제를 풀어낼 수 있는가??

  1. 문제를 크기가 작은문제부터 차례대로 푼다

    for(int i=0;i<n;i++)

  2. 문제의 크기를 점점 크게 만들면서 푼다




1로 만들기 (1463번 문제)

이 문제의 경우 3가지의 행동이 가능하다. 문제를 다이나믹 프로그래밍으로 풀기 위해 작은 문제들로 쪼개어 로직을 구성하면 다음과 같다.

  1. X가 3으로 나누어 떨어지면, 3으로 나눈다 : N->N/3 => D[N] -> D[N/3] + 1
  2. X가 2로 나누어 떨어지면, 2로 나눈다 : N->N/2 => D[N] -> D[N/2] + 1
  3. 1을 뺀다 : N->N-1 => D[N] -> D[N-1] + 1

최소의 결과값을 얻기 위해서는 항상 최소의 과정이 필요하다 따라서 D[N] 을 계산하는 과정은 항상 최소가 되어야 한다. 결과적으로 점화식은 다음과 같은 형태를 하게 된다.

D[N] = min(D[N/3]+1,D[N/2]+1,D[N-1]+1) = min(D[N/3],D[N/2],D[N-1]) + 1

이 문제의 시간복잡도는 전체문제의 개수(배열의 크기) * 1문제를 푸는 시간(O(1)) 이므로 O(N) 이다.


코드는 다음과 같다

#include <stdio.h>
int dp[1000001];

int r(int n) {
    int res;
    if(n==1) return 0; // 1은 아무런 행동도 하지 않고 끝난다
    if(dp[n]>0) return dp[n];   // 메모되어 있다면 꺼낸다
    dp[n] = r(n-1) +1;
    if(n%3==0) {
        int temp = r(n/3) + 1;
        if(temp<dp[n]) dp[n] = temp;
    }
    if(n%2==0) {
        int temp = r(n/2) + 1;
        if(temp<dp[n]) dp[n] = temp;
    }
    return dp[n];
}

int main() {
    int n;
    scanf("%d",&n);
    printf("%d",r(n));
}




2Xn 타일링 (11726번 문제)

문제를 점화식으로 파악하는 것이 가장 중요하다.

이 문제에서는 2XN 크기의 타일을 채울 수 있는 경우의 수를 D[N]이라고 잡자. 2X1 타일을 맨 뒤에 붙인다고 생각하면, 2X(N-1) 만큼의 타일이 남는다. 이것은 D[N-1] 이라고 할 수 있다. 또 1X2 타일 두개를 맨 뒤에 붙인다고 생각하면 2X(N-2) 만큼의 타일이 남고, 이것은 D[N-2] 이다.

따라서 점화식은

D[N] = D[N-1] + D[N-2]

라고 할 수 있다.

또한 여기서 D[1] (즉 2X1) 일 때는 무조건 1가지의 경우의 수이기 때문에 D[1] = 1이다. D[2] 는 가로로 두개를 놓거나 세로로 두개를 넣는 경우 딱 두가지로 나오기 때문에 D[2] = 2 이다. D[3] 부터는 D[N] = D[N-1] + D[N-2] 점화식이 적용될 수 있다. 코드를 짜보면 다음과 같다.

#include <stdio.h>
int dp[1001];

int r(int n) {
    if(n==1) return 1;
    if(n==2) return 2;
    if(dp[n]>0) return dp[n];
    dp[n] = r(n-1)+r(n-2);
    if(dp[n]>10007) dp[n] %= 10007;
    return dp[n];
}

int main() {
    int n;
    scanf("%d",&n);
    printf("%d",r(n));
}




2Xn 타일링2 (11727번 문제)

위의 문제와 완벽하게 동일하지만, 추가할 수 있는 2x2 타일이 추가되었다. 2x2 타일이 맨 뒤에 오는 경우를 생각해보면 D[N-2] 와 같다. 따라서 점화식은 D[N] = D[N-1] + 2*D[N-2] 로 나타낼 수 있다. 또 D[1] = 1 은 변하지 않지만, D[2] 는 가로 블럭이 두개 오는경우, 세로 블럭이 두개 오는 경우, 2x2 타일이 한개 오는 경우로 D[2] = 3 으로 나타낼 수 있다.

코드는 다음과 같다

#include <stdio.h>
int dp[1001];

int r(int n) {
    if(n==1) return 1;
    if(n==2) return 3;
    if(dp[n]>0) return dp[n];
    dp[n] = r(n-1)+2*r(n-2);
    if(dp[n]>10007) dp[n] %= 10007;
    return dp[n];
}

int main() {
    int n;
    scanf("%d",&n);
    printf("%d",r(n));
}




1,2,3 더하기 (9095번 문제)

다이나믹 프로그래밍으로 풀어보자. 먼저 n 을 만들 수 있는 경우의 수를 D[N] 이라고 한다면, 마지막에 1을 더하고 끝난 것은 D[N-1], 2를 더하고 끝난 것은 D[N-2], 3을 더하고 끝난 것은 D[N-3] 이라고 할 수 있다. 그래서 점화식은 D[N] = D[N-1] + D[N-2] + D[N-3] 이라고 할 수 있다. 단 1을 만들 수 있는 경우의 수는 1이므로 D[1] = 1 이고, 2를 만들 수 있는 방법은 D[2] = 2, 3을 만들 수 있는 방법은 (3,2+1,1+2,1+1+1) 4가지 이므로 D[3] = 4 이다.

코드는 다음과 같다

 #include <stdio.h>
int dp[11];

int r(int n) {
    if(n==1) return 1;
    if(n==2) return 2;
    if(n==3) return 4;
    if(dp[n]>0) return dp[n];
    dp[n] = r(n-1) + r(n-2) + r(n-3);
    return dp[n];
}

int main() {
    int t,n;
    scanf("%d",&t);
    while(t--) {
        scanf("%d",&n);
        printf("%d\n",r(n));
    }
}




1,2,3 더하기3 (15988번 문제)

위 문제와 똑같으나 범위가 늘어났다. 여기서 중요한 점은 int 형으로는 계산할 수 없기 때문에 long long 을 써야 한다

코드는 다음과 같다

#include <stdio.h>
long long dp[1000001];
long long m = 1000000009;

long long r(int n) {
    if(n==1) return 1;
    if(n==2) return 2;
    if(n==3) return 4;
    if(dp[n]>0) return dp[n];
    dp[n] = r(n-1) + r(n-2) + r(n-3);
    dp[n] %= m;
    return dp[n];
}

int main() {
    int t,n;
    scanf("%d",&t);
    while(t--) {
        scanf("%d",&n);
        printf("%lld\n",r(n));
    }
}




카드 구매하기 (11052번 문제)

이 문제는 다음과 같이 접근할 수 있다. 카드 i 개를 구매하는 최대 비용을 D[i] 라고 한다면, 카드팩 j 를 구매하고 남은 i-j 개의 카드를 고르는 최대 비용은 D[i-j] 이다. 따라서 점화식은 D[i] = P[j] + D[i-j] 라고 할 수 있다. 그런데 여기서 중요한 점은 j 의 범위를 꼭 기억해야 한다는 것이다. 여기서 j 의 범위는 1<=j<=i 이다. j 가 0 이 되는 것은 D[i]=D[i] 로 의미없는 식이 되기 때문에 1부터 시작한다. 이렇게 점화식 안에 또다른 변수가 나타난 경우에는 모든 경우의 수를 다 구해보고, 그 중에 해당하는 값을 골라야 한다. 따라서 완성된 점화식은 다음과 같다.

D[i] = max(P[j] + D[i-j]) where (1<=j<=i)`

코드는 다음과 같다

#include <stdio.h>
#include <algorithm>
#include <vector>
using namespace std;
int arr[1001];
int dp[1001];

int r(int n) {
    if(n==0) return 0;
    if(dp[n]>0) return dp[n];
    dp[n] = 0;
    for(int i=1;i<=n;i++) {
        int temp = arr[i]+r(n-i);
        if(temp>dp[n]) dp[n] = temp;
    }
    return dp[n];
}


int main() {
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%d",&arr[i]);
    }
    printf("%d",r(n));
}




카드 구매하기 (11052번 문제)

위의 문제와 완벽하게 동일하나, 최솟값을 구하는 문제이다. 이때 최솟값을 구할 때 초기 값을 -1 로 정하고 만약 -1 이라면 무조건 다음 값을 최솟값으로 만들어 주는 과정만 추가해 주면 된다.

코드는 다음과 같다

#include <stdio.h>
#include <algorithm>
#include <vector>
using namespace std;
int arr[1001];
int dp[1001];

int r(int n) {
    if(n==0) return 0;
    if(dp[n]>0) return dp[n];
    dp[n] = -1;
    for(int i=1;i<=n;i++) {
        int temp = arr[i]+r(n-i);
        if(dp[n]==-1 || temp<dp[n]) dp[n] = temp;
    }
    return dp[n];
}


int main() {
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%d",&arr[i]);
    }
    printf("%d",r(n));
}




1,2,3 더하기 5 (15990번 문제)

이 문제에서는 같은 수를 두번 이상 연속해서 사용하면 안된다. 그렇다면 점화식이 달라지게 된다. 그냥 다이나믹 프로그래밍으로 기존 점화식을 사용하면 안된다.

점화식을 세개로 쪼갠다

D[i][j] = i 를 1,2,3 의 합으로 나타내는 방법의 수, 마지막에 사용한 수는 j

여기서 마지막에 사용한 수라는 것은 i 를 만들기 위해서 사용한 마지막의 바로 전 문자열이라는 것이다. 따라서 j 를 마지막에서 두번째에 사용했기 때문에, 마지막에는 사용할 수 없다.

D[i][1] = D[i-1][2] + D[i-1][3]
D[i][2] = D[i-2][1] + D[i-2][3]
D[i][3] = D[i-3][1] + D[i-3][2]

이 문제에서 점화식만 세우면 큰 어려움은 없는데, 나눈 나머지를 구해야 하므로, 모든 과정에서 %= 를 적용해주고 마지막 결과값 출력에도 %= 를 해주어야 한다.

코드는 다음과 같다

#include <stdio.h>
long long dp[100001][4];
long long mod = 1000000009;

long long r(int n,int idx) {
    if(idx==1) {
        if(n<1) return 0;
        if(n==1) return 1;
        if(dp[n][1]>0) return dp[n][1];
        dp[n][1] = r(n-1,2)+r(n-1,3);
        dp[n][1] %= mod;
        return dp[n][1];
    }
    else if(idx==2) {
        if(n<2) return 0;
        if(n==2) return 1;
        if(dp[n][2]>0) return dp[n][2];
        dp[n][2] = r(n-2,1)+r(n-2,3);
        dp[n][2] %= mod;
        return dp[n][2];
    }
    else if(idx==3) {
        if(n<3) return 0;
        if(n==3) return 1;
        if(dp[n][3]>0) return dp[n][3];
        dp[n][3] = r(n-3,1)+r(n-3,2);
        dp[n][3] %= mod;
        return dp[n][3];
    }
    return 0;
}

int main() {
    int t,n;
    scanf("%d",&t);
    while(t--) {
        scanf("%d",&n);
        printf("%lld\n",(r(n,1)+r(n,2)+r(n,3))%mod);
    }
}




쉬운 계산 수 (10844번 문제)

먼저 점화식을 세우기 위해 규칙을 찾아본다.

D[N] = 길이가 N인 *계단 수*의 개수 (여기서 계단수란 옆자리와의 값이 1씩 차이나는 수) 라고 정의하게 되면 문제가 발생한다. 계단수를 만들기 위해서 옆자리와 비교를 할 수 있어야 하는데, 위 점화식으로는 불가능하다. 따라서 임의의 수를 맨 마지막에 넣고, 그 앞자리에 무엇이 올 수 있는지에 대한 점화식으로 쪼개서 생성해야 한다.

만약 D[N][L] 이 길이가 N인 계산수이고 마지막 숫자가 L 인 식이라고 하자. 그러면 마지막 바로 전 숫자는 L-1 또는 L+1 이 올 수 있다. 따라서 점화식은 다음과 같이 생성된다.(예외처리 포함)

D[N][L] = D[N-1][L-1](L>0) + D[N-1][L+1](L<9)

이에 맞추어 코드를 생성해주면 된다. 예외처리를 잘 생각해야 한다. 맨 앞에는 0이 올 수 없고(D[1][0] 은 없다) 1자리수를 제외한 마지막 자리들에는 0이 들어갈 수 있다.

#include <stdio.h>
long long dp[101][10];
long long mod = 1000000000;

long long r(int n, int idx) {
    if(n==1 && idx!=0) return 1;
    else if(n==1 && idx==0) return 0; // 맨앞에 오는 수가 0일 수 없다
    if(dp[n][idx]>0) return dp[n][idx];
    dp[n][idx] = 0;
    if(idx>0) dp[n][idx] += r(n-1,idx-1);
    if(idx<9) dp[n][idx] += r(n-1,idx+1);
    dp[n][idx] %= mod;
    return dp[n][idx];
}

int main() {
    int n;
    long long sum = 0;;
    scanf("%d",&n);
    for(int i=0;i<10;i++) { // 마지막 자리에는 0이 올 수 있다.
        sum += r(n,i);
        sum %= mod;
    }
    printf("%lld",sum);
}




오르막 수 (11057번 문제)

D[N][L] 을 마지막 숫자가 L인 N자리수 수라고 한다면, 마지막 전 숫자는 L 이하인 모든 수가 올 수 있다. 따라서 점화식은 D[N][L] = D[N-1][K](K<=L) 로 세울 수 있다. 또한 맨 앞자리 수가 0으로 시작할 수 있기 때문에, 항상 K를 0이상 L이하로 생각하고 풀면 된다. 초기화는 D[1][L] = 1 이다.

코드는 다음과 같다

#include <stdio.h>
int dp[1001][10];
int mod = 10007;

int r(int n,int idx) {
    if(n==1) return 1;
    if(dp[n][idx]>0) return dp[n][idx];
    dp[n][idx] = 0;
    for(int i=0;i<=idx;i++) {
        dp[n][idx] += r(n-1,i);
        dp[n][idx] %= mod;
    }
    return dp[n][idx];
}

int main() {
    int n,sum=0;
    scanf("%d",&n);
    for(int i=0;i<10;i++) {
        sum+= r(n,i);
        sum%= mod;
    }
    printf("%d",sum);  
}




이친수 (2193번 문제)

이친수에서는 0으로 시작하지 않고, 1이 연속으로 나오지 않는다. 이 문제도 전형적인 점화식 쪼개주는 문제이다. D[N][0] = N자리 이친수인데, 마지막 수가 0인 개수D[N][1] = N자리 이친수인데, 마지막 수가 1인 개수 로 나누어 볼 수 있다. 따라서 점화식은 다음과 같다.

D[N][0] = D[N-1][0] + D[N-1][1]
D[N][1] = D[N-1][0]

0으로 시작하지 않기 때문에, D[1][0] = 0, D[1][1] = 1 이다.

또한 이 문제는 0,1 만 존재하기 때문에 2차원으로 풀지 않고 1차원으로도 풀 수 있다. 다음과 같이 나타낼 수 있다.

D[i] = D[i-1] // 바로 전에는 모두 다 올 수 있다 (0인경우)
D[i] = D[i-2] // 바로 전에는 0만 올 수 있고, 두번째 전에는 모두 올수 있기 때문이다 (1인경우)

이 문제의 경우에는 우연히 1차원으로 풀 수 있었다. 그러나 모든 문제가 이러한 풀이 방식으로 가능하지는 않기 때문에, 연습할 때는 항상 2차원으로 만들어서 푸는 것을 추천한다!(대부분의 점화식 쪼개기 문젠는 2차원 범위에서 해결이 가능하다.

이 문제의 값은 int 형의 범위를 벗어나기 때문에 long long 을 써 주어야 한다

#include <stdio.h>
long long dp[91][2];

long long r(int n, int idx) {
    if(n==1 && idx==0) return 0;
    if(n==1 && idx==1) return 1;
    if(dp[n][idx]>0) return dp[n][idx];
    dp[n][idx] = 0;
    if(idx==0) {
        dp[n][idx] += r(n-1,1)+r(n-1,0);
    }
    else if(idx==1) {
        dp[n][idx] += r(n-1,0);
    }
    return dp[n][idx];
}

int main() {
    int n;
    scanf("%d",&n);
    printf("%lld",r(n,0)+r(n,1));
}




스티커 (9465번 문제)

이 문제가 다이나믹 프로그래밍이 되는 이유는, 마지막에 뜯는 스티커가 같다고 생각한다면, 그 전까지의 최댓값이 결과의 최댓값과 같기 때문이다.

D[N] = 뗄 수 있는 스티커들의 최댓값 이라고 하자. 그렇다면 최댓값을 구하기 위해서 하나의 스티커를 뜯었을 때, 그 이후의 결과가 어떻게 나오는지 보자.

맨마지막 column (1x2) 스티커를 뜯는 방법은 세가지가 있다.

D[i][j] 를 2xi 에서 얻을 수 있는 최대 점수라고 생각할 때, j 는 이전에 column 에서 어떤 스티커를 떼어냈는지를 나타낸다.

  1. 아예 뜯지 않는다(j==0) => D[i][0] = max(D[i-1][0],D[i-1][1],D[i-1][2])
  2. 위에만 뜯는다(j==1) => D[i][1] = A[i][0] + max(D[i-1][0],D[i-1][2])
  3. 밑에만 뜯는다(j==2) => D[i][2] = A[i][1] + max(D[i-1][0],D[i-1][1])

이 문제는 Top-down 방식의 재귀 함수로 풀게 되면 시간초과가 뜨게 된다. 또한 주의할 점은 2xn 판에서 최댓값을 구하는 것은 n 일때의 D[n][0],D[n][1],D[n][2] 중에서 고르는 것이 아니라 1부터 n까지 모두 중 최댓값을 구해야 한다는 것이다.

코드는 다음과 같다

#include <stdio.h>
#define max(a,b) (((a)>(b))?(a):(b))
long long a[100001][2];
long long d[100001][3];
int main() {
    int t;
    scanf("%d",&t);
    while(t--) {
        int n;
        scanf("%d",&n);
        for (int i=1; i<=n; i++) {
            scanf("%lld",&a[i][0]);
        }
        for (int i=1; i<=n; i++) {
            scanf("%lld",&a[i][1]);
        }
        d[0][0] = d[0][1] = d[0][2] = 0;
        for (int i=1; i<=n; i++) {
            d[i][0] = max(d[i-1][0],max(d[i-1][1],d[i-1][2]));
            d[i][1] = max(d[i-1][0],d[i-1][2])+a[i][0];
            d[i][2] = max(d[i-1][0],d[i-1][1])+a[i][1];
        }
        long long ans = 0;
        for (int i=1; i<=n; i++) {
            ans = max(max(ans,d[i][0]),max(d[i][1],d[i][2]));
        }
        printf("%lld\n",ans);
    }
}




포도주 시식 (2156번 문제)

연속으로 놓여진 3잔은 마실 수 없다. 이 문제도 경우의 수를 나누어서 생각할 수 있다. D[N][L] 에서 L은 이번(마지막)에 잔을 마시지 않거나(0), 마시는데 이번으로 연속 1번(1)이거나, 이번으로 연속 2번(2)이거나로 나누어서 점화식을 세워본다.

D[N][0] = 전에 연속으로 마시지 않음 = max(D[N-1][0],D[N-1][1],D[N-2][2])
D[N][1] = 이번에 마셔서 1번 연속 = D[N-1][0]
D[N][2] = 이번에 마셔서 2번 연속 = D[N-1][1]

dp 문제(백준)를 시간 초과 없이 풀려면 아무래도 top-down 방식보다 bottom-up 방식이 더 빠른 것 같다. 이제부터 bottom-up 으로 풀려고 한다.

#include <stdio.h>
#define max(a,b) (((a)>(b))?(a):(b))
int a[10001];
int dp[10001][3];
// D[n][0] = max(D[n-1][0],D[n-1][1],D[n-1][2])
// D[n][1] = a[n] + D[n-1][0]
// D[n][2] = a[n] + D[n-1][1]
// D[1][0] = 0
// D[1][1] = a[1]
// D[1][2] = 0 <- 불가능

int main() {
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
    }
    dp[1][0] = 0;
    dp[1][1] = a[1];
    dp[1][2] = 0;
    for(int i=2;i<=n;i++) {
        dp[i][0] = max(dp[i-1][0],max(dp[i-1][1],dp[i-1][2]));
        dp[i][1] = a[i] + dp[i-1][0];
        dp[i][2] = a[i] + dp[i-1][1];
    }
    int res = 0;
    for(int i=1;i<=n;i++) {
        if(dp[i][0]>res) res = dp[i][0];
        if(dp[i][1]>res) res = dp[i][1];
        if(dp[i][2]>res) res = dp[i][2];
    }
    printf("%d",res);
}




가장 긴 증가하는 부분수열 (11053번 문제)

Longest Increasing Subsequence

수열에서 몇개를 순서대로 빼내어 가장 긴 증가하는 수열을 만드는 문제이다.


수열에서 부분수열을 만드는 방법

2^N 개가 있다. 이 문제는 N<=1000 이기 때문에 2^1000 까지 다 구하는 것은 불가능하다.


다이나믹 프로그래밍으로 푸는 방법

D[i] = A[i]를 마지막으로 하는 가장 긴 증가하는 부분수열 이라고 하자. 그렇다면 그 뒤에 올 수 있는 최대값을 만드는 뒷 부분 수열은 항상 같다. 구하는 방법은 다음과 같다.

A = {10, 20, 10, 30, 20, 50,10} 이라고 하자. 순차적으로 1개부터 N개까지 D[N] 을 구해본다. 여기서 하나의 D[N] 을 구할때는 이전의 D[N-1] ~D[1] 중 만족하는 가장 큰 녀석을 자신에게 더해준다. 따라서 순서는 N-1 부터 1 순으로 찾아간다.

  1. D[1] 은 스스로 증가하는 부분수열을 만족하기 때문에 1이다.
  2. D[2] 는 스스로 만족 & D[1] 을 포함하여 만족하므로 1+D[1] = 2 이다.
  3. D[3] 은 스스로만 만족하므로 1이다.
  4. D[4] 는 스스로 만족 & D[3] 도 가능하고 D[2] 도 가능하고 D[1]도 가능하다. 이 중 가장 값이 큰 녀석을 저장하기 때문에 3이 저장된다.
  5. D[5] 는 스스로 & D[3],D[1] 을 만족하므로 2가 저장된다.
  6. D[6] 은 스스로 & D[5~1] 다되기 때문에 그 중 가장 큰 값을 저장하여 4가 된다.
  7. D[7]은 스스로만 만족하므로 1이 저장된다.

D[] = {1,2,1,3,2,4,1}

이 LIS 문제에서 정답은 4가 된다. 이 문제에서 가장 큰 D[N] 이 정답이 된다.


코드는 다음과 같다

#include <stdio.h>
int a[1001];
int dp[1001];

int main() {
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
    }
    for(int i=1;i<=n;i++) {
        dp[i] = 1;
        for(int j=1;j<i;j++) {
            if(a[j]<a[i] && dp[j]+1>dp[i]) {
                dp[i] = dp[j]+1;
            }
        }
    }
    int res = 0;
    for(int i=1;i<=n;i++) {
        if(dp[i]>res) res = dp[i];
    }
    printf("%d",res);
}




가장 긴 증가하는 부분 수열4 (14002)

위 문제와 정확하게 동일하게 가장 긴 증가하는 부분 수열을 찾는 문제이지만, 그 긴 수열 자체를 출력하는 문제이다.


이 문제에서는 앞에서 한 과정에 기록하는 과정을 추가해야 한다. 앞의 D[i] 에 의해 현재 D[N] 이 바뀔 때, V[N] 에는 i (인덱스) 를 기록해주어 어떤 D[i] 에 의해 영향을 받았는지 기록해준다.

image.png

인덱스를 보고 쭉쭉 거꾸로 따라가다 보면(V[i] 가 0이 나올때까지), 앞의 모든 수열들을 찾아낼 수 있다. 찾아낼 때 재귀함수를 이용하면 순서대로 나오고, 그냥 반복으로 찾아주면 거꾸로 나오기 때문에 뒤집어 줘야 한다.

코드는 다음과 같다

#include <stdio.h>
int a[1001];
int dp[1001];
int v[10001];

void r(int n) { // 뒤집어주는 부분이다
    if(n==0) return;
    r(v[n]);
    printf("%d ",a[n]);

}

int main() {
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
    }
    for(int i=1;i<=n;i++) {
        dp[i] = 1;
        for(int j=1;j<i;j++) {
            if(a[j]<a[i] && dp[j]+1>dp[i]) {
                dp[i] = dp[j]+1;
                v[i] = j;
            }
        }
    }
    int res = 0;
    int l = 0;
    for(int i=1;i<=n;i++) {
        if(dp[i]>res) {
            res = dp[i];
            l = i;
        }
    }
    printf("%d\n",res);
    r(l);
}




가장 큰 증가하는 부분 수열 (11055번 문제)

이 문제는 다 똑같은데 개수 대신 합 이 가장 큰 수열을 고르는 문제이다. 간단하게 개수를 구하는 부분을 합을 구하도록 바꾸기만 하면 된다.

코드는 다음과 같다

#include <stdio.h>
int a[1001];
int dp[1001];

int main() {
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
    }
    for(int i=1;i<=n;i++) {
        dp[i] = a[i];
        for(int j=1;j<i;j++) {
            if(a[j]<a[i] && dp[j]+a[i]>dp[i]) {
                dp[i] = dp[j]+a[i];
            }
        }
    }
    int res = 0;
    for(int i=1;i<=n;i++) {
        if(dp[i]>res) res = dp[i];
    }
    printf("%d",res);
}




가장 긴 감소하는 부분 수열 (11722번 문제)

먼저 A[i] 에서 시작하는 가장 긴 감소하는 부분 수열을 구하는 방식으로 풀 수 있다. 또는 더 쉬운 방법으로는 뒤집어서 가장 긴 증가하는 부분 수열을 찾고, 다시 뒤집으면 된다.

처음에 받을 때만 반대로 받고 가장 긴 증가하는 부분 수열의 개수를 찾아주면 된다.

for(int i=n;i>0;i--) {
    scanf("%d",&a[i]);
}




가장 긴 바이토닉 부분 수열 (11054번 문제)

가장 긴 증가하는 부분수열(D1)과 가장 긴 감소하는 부분 수열(D2)을 구한다. 결과는 D1 + D2 - 1 이 된다.

  1. 증가하는 부분수열의 개수를 모두 찾는다
  2. 감소하는 부분수열의 개수를 모두 찾는다 (뒤집어서 했음)
  3. 두개를 합친다음 1씩 빼서 저장한 것 중 최댓값을 찾는다

코드는 다음과 같다

#include <stdio.h>
#include <algorithm>
using namespace std;
int a[1001];
int ra[1001];
int dp1[1001];
int dp2[1001];

int main() {
    int n,temp;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%d",&temp);
        a[i] = temp;
        ra[n-i+1] = temp;
    }
    for(int i=1;i<=n;i++) {
        dp1[i] = 1;
        dp2[i] = 1;
        for(int j=1;j<i;j++) {
            if(a[j]<a[i] && dp1[j]+1>dp1[i]) {
                dp1[i] = dp1[j]+1;
            }
            if(ra[j]<ra[i] && dp2[j]+1>dp2[i]) {
                dp2[i] = dp2[j]+1;
            }
        }
    }
    reverse(dp2+1,dp2+n+1);
    int res = 0;
    for(int i=1;i<=n;i++) {
        if(dp1[i]+dp2[i]-1>res) {
            res = dp1[i]+dp2[i]-1;
        }
    }
    printf("%d",res);
}




연속합 (1912번 문제)

연속된 숫자들을 뽑아서 가장 큰 합을 구하는 문제이다.

각각의 수가 연속합에 포함되었을 때의 최댓값을 찾는 방식으로 풀어나가면 된다.

D[i] = A[i] 에서 끝나는 연속합의 최대 라고 하자. 즉 D[i] = A[i] + D[i-1] 이라고 생각할 수 있다. 아니면 자기 자신 혼자만 존재하는 D[i] = A[i] 라고 할 수도 있다. 따라서 점화식은 D[i] = max(A[i],A[i]+D[i-1]) 이라고 할 수 있다.

코드는 다음과 같다

#include <stdio.h>
long long a[100001];
long long dp[100001];

int main() {
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%lld",&a[i]);
    }
    dp[1] = a[1];
    for(int i=2;i<=n;i++) {
        if(a[i]+dp[i-1]>a[i]) dp[i] = dp[i-1]+a[i];
        else dp[i] = a[i];
    }
    long long res = dp[1];
    for(int i=2;i<=n;i++) {
        if(dp[i]>res) res = dp[i];
    }
    printf("%lld",res);
}




연속합2 (13398번 문제)

이 문제는 위의 문제와 완벽하게 동일하지만 수1개를 제거할 수 있다. 그냥 쉽게 제거할 수를 하나 선택하고 제거한 다음 위와 같이 구하면 O(백만) 나와서 시간안에 절대로 못푼다!

이 문제는 하나의 제거할 대상을 두고 오른쪽에서부터 dr 을 구하고 왼쪽에서부터 dl 을 구하는 방식으로 풀어나가야 한다. 결과값은 dr 과 dl 을 합친 값이 된다.

  1. 먼저 일반적인 방식으로 왼쪽에서부터 모든 부분합의 값을 구한다.
  2. 오른쪽부터 시작해서 구하는 부분합을 구해놓는다.
  3. 일반적인 부분합과, 하나를 제거하고 왼쪽과 오른쪽을 연결한 부분합 중 가장 큰 값을 결과값에 넣고 출력한다.

코드는 다음과 같다

#include <stdio.h>
long long a[100001];
long long dl[100001];
long long dr[100001];


int main() {
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%lld",&a[i]);
    }
    dl[1] = a[1];
    dr[n] = a[n];
    for(int j=2;j<=n;j++) {
        if(dl[j-1]>0) dl[j] = dl[j-1]+a[j];
        else dl[j] = a[j];
    }
    for(int j=n-1;j>0;j--) {
        if(dr[j+1]>0) dr[j] = dr[j+1]+a[j];
        else dr[j] = a[j];
    }

    long long res = dl[1];
    for(int i=2;i<=n;i++) {
        if(dl[i]>res) res = dl[i];
    }
    for(int i=2;i<n;i++) {
        if(dl[i-1]+dr[i+1]>res) res = dl[i-1]+dr[i+1];
    }
    printf("%lld",res);
}




제곱수의 합 (1699번 문제)

주어진 N을 제곱수들의 합으로 표현할 때 그 항의 최소개수를 구하는 문제이다.

여기서 D[N]은 항의 개수이다. 그런데 더해진 마지막 수가 뭔지를 알 수가 없다. 편의상 i^2 이라고 한다면, D[N] = min(D[N-i^2] +1) 이라고 할 수 있다. 여기서 i^2 은 1보다 커야하고, N보다 작아야 한다. 따라서 점화식은 다음과 같다.

D[N] = min(D[N-i^2] +1) (1<=i^2<=N)

코드는 다음과 같다

#include <stdio.h>
int d[100001];
int main() {
    int n;
    scanf("%d",&n);
    for (int i=1; i<=n; i++) {
        d[i] = i;
        for (int j=1; j*j <= i; j++) {
            if (d[i] > d[i-j*j]+1) {
                d[i] = d[i-j*j]+1;
            }
        }
    }
    printf("%d",d[n]);
}




합분해 (2225번 문제)

0~N까지 정수 K개 더해서 그 합이 N이 되는 경우의 수를 구하는 것이다.

D[K][N] = N까지의 정수 K개를 더해서 N을 만드는 경우의 수 라고 하자. 맨 뒤에 더해진 값이 L이라고 한다면, D[K][N] = sum(D[K-1][L] + 1) (0<=L<=N) 이다.

이 방법은 N^3 의 시간 복잡도가 나오게 되는데, 더 빠르게 풀 수 있는 방법이 있다. 이 점화식을 그림으로 그려보고 D[K][N] 을 파악해보면 다음과 같다.

image.png

DDBFBB60-EF93-440C-9E99-DB304D99EEB6.png

다음과 같이 D[K][N] = D[K][N-1] + D[K-1][N] 이 된다.

코드는 다음과 같다

#include <stdio.h>
long long d[201][201];
long long mod = 1000000000;
int main() {
    int n, k;
    scanf("%d %d",&n,&k);
    d[0][0] = 1;
    for (int i=1; i<=k; i++) {
        for (int j=0; j<=n; j++) {
            for (int l=0; l<=j; l++) {
                d[i][j] += d[i-1][j-l];
                d[i][j] %= mod;
            }
        }
    }
    printf("%lld\n",d[n][k]);
}

Comments