Processing math: 100%
본문 바로가기

[과제] Dynamic programming

1. N을 1, 2, 3의 합으로 나타내기

예를 들어 N=5면 다음과 같이 나타낸다.

4

=1+1+1+1

=1+1+2

=1+2+1

=2+1+1

=2+2

=1+3

=3+1

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static int solve(int n) {
    int[] a = new int[n];
        
    a[0= 0;
    a[1= 1
    a[2= 1
    a[3= 2
        
    //from idx=4 to idx=n
    for(int i = 4; i < n; i++) {
        a[i] = a[i - 2+ a[i - 3+ a[i - 4];
    }
    
    return a[n - 1];
}
cs

인덱스가 헷갈리면 아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
public static int solve(int n) {
    int[] a = new int[n + 1];
    
    a[1= 1;
    a[2= 2;
    a[3= 4
        
    for(int i = 4; i <= n; i++) {
        a[i] = a[i - 1+ a[i - 2+ a[i - 3];
    }
    
    return a[n];
}
cs

주어진 N1+(N1),2+(N2),3+(N3)으로 보면 N1을 만드는 경우의 수, N2를 만드는 경우의 수, N3를 만드는 경우의 수의 합이 된다.

나는 a[i]를 'i1,2,3의 합으로 나타내는 경우의 수'로 정의했으므로 위와 같은 풀이가 성립한다.

만약에 1,2,3이 아닌 3,4,5의 합으로 나타내는 것이 문제였다면 a[i]=a[i3]+a[i4]+a[i5]가 됐을 것.

 

Time complexity: O(n)

 

 

2. 치즈

테이블의 각 요소를 그 위치까지 오면서 먹을 수 있는 치즈의 최대 개수로 정의하면,
a[i][j]의 값을 (1,1)부터 (i,j)까지 이동하면서 먹을 수 있는 치즈의 최대 개수라 하면 다음과 같이 풀 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static int solve(int n, int[][] a) {
    //a[i][j] = (i, j)까지 오면서 먹을 수 있는 치즈의 최대 개수
    
    //Initialization
    for(int i = 1; i < n; i++) {
        a[i][0+= a[i - 1][0]; //1st column
        a[0][i] += a[0][i - 1]; //1st row
    }
 
 
    /*
     * 현재 위치로 오는 경로는 왼쪽으로부터 오거나 밑에서 올라오거나
     * 따라서 그 둘 중 큰 쪽을 선택한다. 
     */
    for(int i = 1; i < n; i++) {
        for(int j = 1; j < n; j++) {
            a[i][j] += Math.max(a[i - 1][j], a[i][j - 1]);
        }
    }
    
    return a[n - 1][n - 1];
}
cs

각 위치로 도달하는 방법은 왼쪽에서 오거나 밑에서 오는 것 뿐이다.

따라서 A[i][j]A[i1][j]A[i][j1] 중 더 큰 값이다.

 

Base case는 테이블의 왼쪽과 아래 가장자리로, 이 부분은 아래에서 올라오거나 왼쪽에서 오는 경우 중 하나만 적용된다.

즉 왼쪽의 가장자리는 아래에서부터 더해 a[i][0]+=a[i1][0]로 초기화하고,

아래의 가장자리는 a[0][i]+=a[0][i1]로 초기화한다.

 

Time complexity: O(n2)

 

 

 

 

 

 

3. 동전

이 문제는 greedy algorithm으로도 풀 수 있(다고 들은 것 같)다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
    int k = 365;    
    int[] coins = new int[] {14713285291365};
    int[] a = new int[k + 1];
 
    System.out.println(solve(a, coins, k));
}
    
/*
 * 최소 금액이 1원보다 큰 경우 나타낼 수 없는 K가 발생한다
 * 나타낼 수 없는 금액은 어떻게 처리하지...
 */
public static int solve(int[] a, int[] coins, int k) {
    a[0= 0//sentinel
    for(int i = 1; i < coins[0]; a[i++= 0);
    for(int i = coins[0]; i <= k; i++) {
        a[i] = a[i - coins[0]] + 1;
        for(int j = 1; j < coins.length && coins[j] <= i; j++) {
            a[i] = Math.min(a[i], a[i - coins[j]] + 1);
        }
    }
 
    return a[k];
}
cs


a[i] = Nadiria의 동전으로 i원을 나타낼 때 필요한 동전의 최소 개수.

15행:
Base case.
최소 금액 미만의 금액은 나타낼 수 없으므로 0.

16행:
i원을 나타내는 데에 필요한 동전의 수는 i에서 어떤 동전 coin[j]의 금액을 뺀 금액을 나타내는 데에 필요한 경우의 수에 coin[j]의 수 1을 더한 것으로 나타낼 수 있다.
i원을 나타내는 데에 필요한 동전의 수는
min{
    (1원짜리 동전 1개 + (k-1)원을 나타내는 동전의 수),
    (4원짜리 동전 1개 + (k-4)원을 나타내는 동전의 수),
    (7원짜리 동전 1개 + (k-7)원을 나타내는 동전의 수),
    (13원짜리 동전 1개 + (k-13)원을 나타내는 동전의 수),
    (28원짜리 동전 1개 + (k-28)원을 나타내는 동전의 수),
    (52원짜리 동전 1개 + (k-52)원을 나타내는 동전의 수),
    (91원짜리 동전 1개 + (k-91)원을 나타내는 동전의 수),
    (365원짜리 동전 1개 + (k-365)원을 나타내는 동전의 수),
}

다.

 

Time complexity: O(K)

주어진 금액 K에 선형적으로 비례함

 

 

 

4. 최대합 부분배열

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
    int[] a = new int[] {31-415926-535897-93-2384};
    System.out.println(solve(a));
}
 
public static int solve(int[] a) {
    //m[i] = sum(a[0..i])
    int[] m = new int[a.length];
    
   m[0= a[0];
    for(int i = 1; i < m.length; i++) {
       m[i] = Math.max(a[i] + m[i - 1], a[i]);
    }
        
    return max(m, 0);
}
cs

7행의 주석에 써놨듯이 m[i]a[0..i]에서 합이 최대인 부분배열의 합이다.
따라서 주어진 n에 대해 m의 길이는 n이다.

 

a를 한 요소 씩 훑으면 다음의 두 선택지가 있다.

1) 현재까지 만들어진 부분배열의 끝에 a[i]를 추가한다.

2) 현재까지 만들어진 부분배열은 모두 버리고 a[i]부터 다시 부분배열을 만든다.

 

첫 번째 경우 m[i]m[i1]+a[i]가 된다.

i에서 현재까지 만들어진 부분배열의 합은 m[i1]이므로 여기에 a[i]를 더하면 된다.

 

두 번째 경우 m[i]a[i]가 된다. 현재까지 만들어진 부분배열 m[i1]은 버리고 a[i]부터 다시 부분배열을 만드는 것이다.

 

합을 최대로 하는 것이 목적이므로 두 경우 중 값이 더 큰 경우를 선택한다.

 

이 과정을 i=n까지 반복하고 나면 m[i]에는 A[0..i]에서 최대합 부분배열의 합이 저장되어 있다.

문제에서 원하는 것은 가장 큰 합이므로 m[0..i]의 최대값이 정답이 된다.

 

 

Time complexity: O(n)

배열 m을 채움 -> O(n)

m에서 최대값을 찾음 -> O(n)

따라서 시간 복잡도는 O(n)이 된다.

 

 

 

5. 최대곱 부분배열

 

딱 보고 최대합 부분배열이랑 똑같이 하면 되겠다 싶었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
    int[] a = new int[] {0140-7050-612-70};
    System.out.println(solve(a));
}
 
public static int solve(int[] a) {
    int[] max = new int[a.length];
    int[] min = new int[a.length];
    
    max[0= min[0= a[0];
    for(int i = 1; i < a.length; i++) {
        int[] temp = new int[] { a[i]*max[i - 1], a[i], a[i]*min[i - 1] };
        max[i] = max(temp, 0);
        min[i] = min(temp, 0);
    }
    
    return max(max, 0);
}
cs

어림도 없지

 

핵심은 두 개의 배열이다.

0 이상의 수만 있다면 최대합 부분배열이랑 똑같이 해도 상관 없지만 문제에서는 음수도 포함하고 있다.

이 음수가 문제다. 음수끼리 곱하면 더 큰 양수가 될 수 있기 때문에 이 경우를 따로 생각해줘야 한다. 
때문에 두 개의 배열이 필요하다. 한 배열에는 최대값을, 한 배열에는 최소값을 저장한다.

최소값에 음수를 곱하면 최대값이 될 수 있기 때문이다.

 

따라서 다음의 세 선택지가 있다.

1) 현재까지 만들어진 최대값(양수)에 a[i]를 곱한다.

2) 현재까지 만들어진 부분배열을 버리고 a[i]부터 다시 부분배열을 만든다.

3) 현재까지 만들어진 최소값(음수)에 a[i]를 곱한다.

 

max에는 이 세 선택지 중 값이 가장 큰 경우, min에는 가장 값이 작은 경우가 저장된다.

따라서 max에 의해 양수와 양수의 곱이 최대인 경우를, min에 의해 음수와 음수의 곱이 최대인 경우를 모두 생각할 수 있다.

 

Time complexity: O(n)