Group Study (2022-2023)/Algorithm

[Algorithm] 5주차 스터디 - 다이나믹 프로그래밍

우주호 2022. 11. 12. 19:50

Dynamic Programming

다이나믹 프로그래밍(DP) 즉, 동적 계획법은 하나의 큰 문제를 여러 개의 작은 문제로 나누어서 그 결과를 저장하고 다시 큰 문제를 해결할 때 사용하는 기법이다.

DP는 재귀와 유사하지만 재귀는 사용하면 동일한 계산이 여러번 반복되어 비효율적인 계산이 될 수 있다는 점이 DP와 큰 차이점이다. DP는 재귀의 이러한 단점을 개선해준다. 

DP를 구현하는 방법 중 가장 많이 사용하는 방법은 `Memoization(메모이제이션)`이다. Memoization은 한번 구한 결과를 메모리 공간에 메모해두고 같은 식을 호출하면 메모한 결과를 그대로 가져오는 기법이다. 값을 기록한다는 점에서 `Cashing(캐싱)`이라고도 한다.

DP는 주어진 문제가 DP유형인지 파악하는 것이 중요하다. 따라서 DP를 사용할 때 반복된 계산이 있는지 그리고 메모이제이션을 적용할 수 있는지를 따지고 난 후 사용하면 좋다.


< 11726 -  2xn 타일링 >

https://www.acmicpc.net/problem/11726

 

11726번: 2×n 타일링

2×n 크기의 직사각형을 1×2, 2×1 타일로 채우는 방법의 수를 구하는 프로그램을 작성하시오. 아래 그림은 2×5 크기의 직사각형을 채운 한 가지 방법의 예이다.

www.acmicpc.net

풀이 : 

모든 경우의 수를 생각해보면 규칙을 찾을 수 있다. 

n=1일때) I

n=2일때) II, =

n=3일때) III, =I, I=

n=4일때) IIII, =II, I=I, II=, II=

...

2xn 사이즈의 직사각형을 채우려면 2x(n-1)과 2x(n-2)사이즈의 직사각형을 채우는 방법을 더하면 된다. 따라서 dp[n] = dp[n-1] + dp[n-2]이다.

#include <iostream>

using namespace std;

int main()
{

    int n;
    cin >> n;

    int dp[1001] = {0};
    dp[1] = 1;
    dp[2] = 2;

    for (int i = 3; i <= n; i++)
    {
        dp[i] = (dp[i - 1] + dp[i - 2]) % 10007;
    }

    cout << dp[n];

    return 0;
}

< 2749 -  피보나치 수 3 >

https://www.acmicpc.net/problem/2749

 

2749번: 피보나치 수 3

첫째 줄에 n이 주어진다. n은 1,000,000,000,000,000,000보다 작거나 같은 자연수이다.

www.acmicpc.net

풀이 : 

일반적인 피보나치를 이용해서 풀면 n의 값이 너무 크기 때문에 시간 내에 답을 구할 수 없다. 이 문제는 피보나치의 속성을 몇 가지 알아야 풀 수 있다. 

- 피사노 주기 : 피보나치 수를 K로 나눈 나머지는 항상 주기를 가진다. (ex: 피보나치 수를 3으로 나누었을 때 주기는 8)

- 주기가 t라면 n번째 피보나치 수를 m으로 나눈 나머지는 n%t번째 피보나치 수를 m으로 나눈 나머지와 같다.

- m = 10^k일 때, k가 2보다 크다면 주기는 항상 15 * 10^k -1이다.

따라서 위의 속성들 중 세 번째를 이용하여 M = 1000000이므로 피사노 주기는 15 * 1000000 / 10 인 것을 알 수 있다. 그리고 두 번째 속성을 이용해서 구하려는 값은 n / 1000000 == n % t % 1000000임을 알 수 있다.

#include<iostream>
 
long long a[1500050];
int INF = 1000000;
 
void fibonacci() {
    a[0] = 0;
    a[1] = 1;
    for (int i = 0; i < 1500000; i++) {
        a[i + 2] = (a[i + 1] + a[i]) % INF;
    }
}
 
int main() {
    long long n;
    std::cin >> n;
    fibonacci();
    std::cout << a[n%1500000] << "\n";
 
}

< 11053 -  가장 긴 증가하는 부분 수열 >

https://www.acmicpc.net/problem/11053

 

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

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오. 예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이

www.acmicpc.net

풀이 : 

첫 번째 인덱스부터 검사하면서 dp리스트에 자신을 포함하여 만들 수 있는 부분 수열 크기를 저장한다. 현재 위치와 이전에 있는 원소를 크기 비교하여 크다면 현재 위치의 dp값에 1을 더해준다. 그리고 dp에 있는 값들 중 최댓값을 출력한다.

#include <iostream>
#include <vector>
#include <algorithm>

#pragma warning(disable: 4996)

using namespace std;

int main() {
    int T;
    int N, M, K, H;
    int X, Y;
    int answer = 0;
    
    int dp[1001];
    vector<int> v;
    cin >> T;

    for (int i = 0; i < T; i++) {
        cin >> K;
        v.emplace_back(K);
        int dp_max = 0;

        for (int j = 0; j < v.size(); j++) {
            if (v[i] > v[j]) {
                if (dp_max < dp[j])
                    dp_max = dp[j];
            }
        }
        dp[i] = dp_max + 1;
        answer = max(answer, dp[i]);
    }

    cout << answer << endl;

    return 0;
}

< 11049 -  행렬 곱셈 순서 >

https://www.acmicpc.net/problem/11049

 

11049번: 행렬 곱셈 순서

첫째 줄에 입력으로 주어진 행렬을 곱하는데 필요한 곱셈 연산의 최솟값을 출력한다. 정답은 231-1 보다 작거나 같은 자연수이다. 또한, 최악의 순서로 연산해도 연산 횟수가 231-1보다 작거나 같

www.acmicpc.net

풀이 : 

A, B, C, D라는 행렬이 있을 때 A*B*C*D의 경우 A*B, B*C, C*D의 곱을 먼저 구하였다면 이후에도 그 전에 구한 값을 사용할 수 있다. 따라서 가장 적게 계산을 반복하는 경우를 DP배열에 저장하고 마지막에 출력한다. 

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
#define INF 1000000000

int N, r, c;
int sum[501], matrix[501][2], dp[501][501];

int main()
{
	cin >> N;

	for (int i = 1; i <= N; i++)
	{
		cin >> r >> c;
		matrix[i][0] = r;
		matrix[i][1] = c;
	}

	for (int i = 1; i < N; i++)
	{
		for (int j = 1; i + j <= N; j++)
		{
			dp[j][i + j] = INF;
			for (int k = j; k <= i + j; k++)
			{
				dp[j][i + j] = min(dp[j][i + j], dp[j][k] + dp[k + 1][i + j] + matrix[j][0] * matrix[k][1] * matrix[i+j][1]);
			}
		}

	}

	cout << dp[1][N];


}