DataStructure/Algorithm

Dynamic programming 연습문제

S0LL 2024. 10. 17. 01:22

문제 1: 다이나믹 프로그래밍의 기본 개념

 

다이나믹 프로그래밍이 적용될 수 있는 문제의 두 가지 핵심 특성을 설명하고, 각각의 특성이 왜 중요한지 간략히 서술하시오.

1.	중복되는 하위 문제 (Overlapping Subproblems):
•설명: 문제를 해결하는 과정에서 동일한 하위 문제가 여러 번 반복하여 계산됩니다.
•중요성: 중복 계산을 효율적으로 처리하기 위해 하위 문제의 결과를 저장(메모이제이션)하고 재사용할 수 있습니다. 
이는 전체 문제의 시간 복잡도를 크게 줄여줍니다.

2.	최적 부분 구조 (Optimal Substructure):
•설명: 문제의 최적 해가 하위 문제들의 최적 해로부터 구성될 수 있습니다.
•중요성: 최적 해를 구성하는 과정에서 하위 문제의 최적 해를 활용할 수 있으므로, 
전체 문제의 최적 해를 효율적으로 찾을 수 있습니다.

 

 

문제 2: 피보나치 수열의 메모이제이션 구현

 

다음 피보나치 수열을 다이나믹 프로그래밍의 메모이제이션 기법을 사용하여 C++로 구현하시오. 함수 fibonacci_dp는 정수 n을 입력받아 n번째 피보나치 수를 반환해야 합니다. 코드에 주석을 달아 각 부분의 역할을 설명하시오.

#include <iostream>
#include <vector>
using namespace std;

// 피보나치 수를 메모이제이션을 사용하여 계산하는 함수
long long fibonacci_dp(int n, vector<long long> &memo) {
    // 여기에 코드를 작성하시오
}

int main() {
    int n = 40;
    vector<long long> memo(n + 1, -1);
    cout << "피보나치(" << n << ") = " << fibonacci_dp(n, memo) << endl;
    return 0;
}
#include <iostream>
#include <vector>
using namespace std;

// 피보나치 수를 메모이제이션을 사용하여 계산하는 함수
long long fibonacci_dp(int n, vector<long long> &memo) {
    if(n<=1) return n;
    
    if(memo[n] != -1)return memo[n];
    
    memo[n] = fibonacci_dp(n-1,memo) + fibonacci_dp(n-2,memo);
    return memo[n];
}

int main() {
    int n = 40;
    vector<long long> memo(n + 1, -1);
    cout << "피보나치(" << n << ") = " << fibonacci_dp(n, memo) << endl;
    return 0;
}

 

문제 3: 이항 계수의 시간 복잡도 분석

 

이항 계수를 재귀적으로 계산하는 함수와 다이나믹 프로그래밍을 사용하여 계산하는 함수의 시간 복잡도를 각각 설명하시오. 왜 다이나믹 프로그래밍 접근이 더 효율적인지 논리적으로 서술하시오.

#이항계수를 재귀적으로 사용하면
C(n,k) = C(n-1,k-1) + C(n-1, k) 로 정의된다.
한 번 호출할 때마다 2번의 재귀가 이루어지므로 호출 트리가 지수적으로 증가하고,
시간 복잡도는 O(2^n)이다.


#DP를 사용하면
C(n,k)를 계산할 떄 이전에 계산했던 것들은 다시 계산할 필요 없이 가져다 쓰면된다.
따라서 시간 복잡도는 O(nk) 이다.

DP를 이용해 하위 문제의 결과를 저장하고 재사용함으로써,
걸리는 시간을 크게 줄일 수 있다.

 

문제 4: Floyd-Warshall 알고리즘의 응용

 

Floyd-Warshall 알고리즘을 사용하여 그래프의 모든 정점 간 최단 경로를 계산할 때, 음수 간선이 있는 경우와 없는 경우의 차이점을 설명하시오. 또한, 음수 사이클이 존재할 때 알고리즘이 어떻게 동작하는지 기술하시오.

•음수 간선이 없는 경우:
•설명: 모든 간선의 가중치가 양수이거나 0인 경우, Floyd-Warshall 알고리즘은 정상적으로 모든 정점 간의 최단 경로를 계산합니다.
•동작: 알고리즘은 각 중간 정점을 추가하면서 dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])를 반복하여 최단 거리를 업데이트합니다.

•음수 간선이 있는 경우:
•설명: 음수 간선이 존재하더라도, 음수 사이클이 없는 한 Floyd-Warshall 알고리즘은 올바른 최단 경로를 계산할 수 있습니다.
•동작: 음수 간선이 있는 경우에도 알고리즘은 최단 거리를 올바르게 업데이트하지만, 음수 사이클이 있는 경우 문제가 발생할 수 있습니다.

•음수 사이클이 존재할 때:
•설명: 그래프 내에 음수 사이클이 존재하면, 최단 경로의 개념이 무한히 작아지므로 의미가 없어집니다.
•동작: Floyd-Warshall 알고리즘은 음수 사이클을 감지할 수 있습니다. 알고리즘 실행 후 dist[i][i]가 음수인 경우, 이는 정점 i를 포함하는 음수 사이클이 존재함을 의미합니다.
•결과: 음수 사이클이 존재하는 경우, 알고리즘은 이를 보고하고, 최단 경로를 제대로 계산할 수 없음을 알려줍니다.

 

문제 5: 0-1 배낭 문제 구현

 

0-1 배낭 문제를 다이나믹 프로그래밍을 이용하여 C++로 구현하시오. 함수 knapsack은 배낭의 무게 제한 W, 물건들의 무게 weights, 물건들의 가치 values, 그리고 물건의 수 n을 입력받아 배낭에 담을 수 있는 최대 가치를 반환해야 합니다. 코드에 상세한 주석을 추가하시오.

#include <iostream>
#include <vector>
using namespace std;

// 0-1 배낭 문제를 다이나믹 프로그래밍으로 해결하는 함수
int knapsack(int W, const vector<int> &weights, const vector<int> &values, int n) {
    // 여기에 코드를 작성하시오
}

int main() {
    int W = 50;
    vector<int> weights = {10, 20, 30};
    vector<int> values = {60, 100, 120};
    int n = weights.size();
    cout << "최대 가치: " << knapsack(W, weights, values, n) << endl;
    return 0;
}
#include <iostream>
#include <vector>
using namespace std;

// 0-1 배낭 문제를 다이나믹 프로그래밍으로 해결하는 함수
int knapsack(int W, const vector<int> &weights, const vector<int> &values, int n) {
    vector<vector<int>> dp(n+1, vector<int>(W+1, 0));
    
    for(int i=0; i<=n; i++){
      for(int w=0; w<=W; w++){
        if(weights[i-1] <= w){
          dp[i][w] = max(dp[i-1][w], values[i-1] + dp[i-1][w-weights[i-1]]);
        }
        else{
          dp[i][w] = dp[i-1][w];
        }
      }
    }
    return dp[n][W];
}

int main() {
    int W = 50;
    vector<int> weights = {10, 20, 30};
    vector<int> values = {60, 100, 120};
    int n = weights.size();
    cout << "최대 가치: " << knapsack(W, weights, values, n) << endl;
    return 0;
}

 

문제 6: 연쇄 행렬 곱셈의 최소 연산 수 계산

 

연쇄 행렬 곱셈 문제에서, 주어진 행렬들의 곱셈 순서를 최적화하여 최소 연산 수를 계산하는 다이나믹 프로그래밍 알고리즘의 시간 복잡도를 분석하시오. 또한, 알고리즘이 어떻게 동작하는지 간략히 설명하시오.

•시간 복잡도 분석:
•외부 루프 (L): 곱셈 범위의 길이를 2부터 n-1까지 증가시킵니다. O(n)
•중간 루프 (i): 각 L에 대해 i는 1부터 n-L까지 반복됩니다. O(n)
•내부 루프 (k): 각 i와 j에 대해 k는 i부터 j-1까지 반복됩니다. O(n)
•총 시간 복잡도: O(n³)

•알고리즘 동작 설명:
•DP 테이블 초기화: dp[i][j]는 i번째 행렬부터 j번째 행렬까지 곱할 때의 최소 연산 수를 저장합니다. 초기에는 모든 dp[i][i] = 0으로 설정합니다.
•곱셈 범위 순회: 행렬의 곱셈 범위 L을 2부터 n-1까지 증가시키면서, 각 범위 내의 i와 j를 설정합니다.
•최적 분할점 찾기: 각 i와 j에 대해 가능한 모든 분할점 k를 고려하여, dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j]의 최소값을 dp[i][j]에 저장합니다.
•결과: 최종적으로 dp[1][n-1]에 전체 행렬을 곱할 때의 최소 연산 수가 저장됩니다.

 

문제 7: 최적 이진 탐색 트리의 시간 복잡도 개선

 

최적 이진 탐색 트리 문제를 해결하는 다이나믹 프로그래밍 알고리즘에서, 빈도 합을 사전에 계산하여 시간 복잡도를 어떻게 개선할 수 있는지 설명하시오. 개선된 알고리즘의 시간 복잡도를 명시하시오.

•기존 알고리즘의 시간 복잡도:
•각 서브트리에 대해 빈도 합을 계산할 때, O(n) 시간이 소요됩니다.
•전체 알고리즘의 시간 복잡도는 O(n⁴)입니다.

•빈도 합 사전 계산의 개선 방법:
•사전 계산: 모든 서브트리에 대한 빈도 합을 미리 계산하여 저장합니다. 이는 sum_freq[i][j]로 표현되며, keys[i]부터 keys[j]까지의 빈도 합을 의미합니다.
•계산 방법: 이중 반복문을 사용하여 sum_freq[i][j] = sum_freq[i][j-1] + freq[j]로 계산합니다.

•개선된 알고리즘의 시간 복잡도:
•빈도 합 계산: O(n²)
•최적 트리 구성: O(n³) (루프가 세 번 중첩됨)
•총 시간 복잡도: O(n³)

 

 

문제 8: 외판원 문제의 메모이제이션 테이블 크기

 

외판원 문제(TSP)를 다이나믹 프로그래밍으로 해결할 때, 메모이제이션 테이블의 크기가 왜 n x 2^n인지를 설명하시오. 여기서 n은 도시의 수입니다.

•상태 정의:
•현재 위치 (pos): 현재 방문 중인 도시의 인덱스 (0 ≤ pos < n).
•방문한 도시의 집합 (mask): 비트마스크를 사용하여 방문한 도시들을 표시 (0 ≤ mask < 2ⁿ).

•메모이제이션 테이블의 크기:
•pos: n개의 가능한 값.
•mask: 각 도시의 방문 여부를 표시하는 2^n개의 가능한 비트마스크.
•따라서, 총 상태 수: n x 2^n

•이유 설명:
•외판원 문제에서는 모든 도시를 방문해야 하므로, 현재 도시와 방문한 도시들의 상태가 문제의 상태를 완전히 정의합니다.
•따라서, 각각의 도시 위치마다 모든 가능한 방문 상태를 저장해야 하므로, 메모이제이션 테이블의 크기는 n x 2^n이 됩니다.

결론:
외판원 문제의 다이나믹 프로그래밍 접근에서,
메모이제이션 테이블의 크기가 n x 2^n인 이유는 각 도시 위치와 모든 가능한 방문 상태를 저장해야 하기 때문입니다.
이는 도시의 수가 증가함에 따라 테이블의 크기가 지수적으로 증가함을 의미합니다.

 

문제 9: 시퀀스 정렬의 공간 복잡도 최적화

 

시퀀스 정렬 문제를 다이나믹 프로그래밍으로 해결할 때, 2차원 DP 테이블 대신 1차원 DP 테이블을 사용하여 공간 복잡도를 최적화하는 방법을 설명하시오. 최적화된 공간 복잡도를 명시하시오.

•기존 2D DP 테이블:
•설명: dp[m+1][n+1] 크기의 2D 테이블을 사용하여 두 시퀀스의 각 글자까지의 최적 정렬 비용을 저장합니다.
•공간 복잡도: O(mn)

•1D DP 테이블로 최적화:
  •이용 방법:
    •현재 행(curr)과 이전 행(prev)만을 저장하는 두 개의 1D 배열을 사용합니다.
    •현재 행을 계산할 때, 이전 행의 값을 참조하고, 현재 행의 값을 업데이트합니다.
    •계산이 완료되면 현재 행을 이전 행으로 업데이트합니다.
  •공간 복잡도: O(n)
    •여기서 n은 두 번째 시퀀스의 길이입니다.
  
•구현 방식:
  •초기화: 이전 행(prev)을 빈 시퀀스로 만드는 비용으로 초기화합니다.
  •행 반복: 각 글자에 대해 삽입, 삭제, 교체 비용을 계산하여 현재 행(curr)을 업데이트합니다.
  •행 교체: 현재 행을 이전 행으로 복사합니다.

결론:
시퀀스 정렬 문제에서 2차원 DP 테이블 대신 두 개의 1차원 배열을 사용하면
공간 복잡도를 O(mn)에서 O(n)으로 줄일 수 있습니다.
이는 메모리 사용량을 절감하고, 큰 시퀀스에 대해서도 효율적으로 처리할 수 있게 해줍니다.

 

문제 10: 다이나믹 프로그래밍 적용 문제

 

다음과 같은 문제를 다이나믹 프로그래밍을 이용하여 해결하는 방법을 설명하시오. 필요한 경우, 점화식을 제시하고 시간 복잡도를 분석하시오.

 

문제: 길이가 n인 이진 문자열이 주어질 때, 문자열 내에서 연속된 1의 개수가 최대 k개를 넘지 않도록 변경(0을 1로, 또는 1을 0으로)하는 최소 변경 횟수를 구하시오.

•문제 이해:
•주어진 이진 문자열에서 연속된 1의 개수가 최대 k개를 넘지 않도록 최소한의 변경(0↔1)을 수행해야 합니다.

•다이나믹 프로그래밍 접근:
•상태 정의:
•dp[i][j]: 첫 i개의 문자까지 고려했을 때, 마지막에 연속된 1의 개수가 j인 상태에서의 최소 변경 횟수.

•점화식:
•현재 문자가 1일 경우:
•연속된 1의 개수가 j-1인 상태에서 j로 증가시키거나, 0으로 변경.
•dp[i][j] = min(dp[i-1][j-1], dp[i-1][any] + 1) (j ≤ k)

•현재 문자가 0일 경우:
•연속된 1의 개수를 유지하지 않고, 0으로 유지.
•dp[i][j] = dp[i-1][any] + 0 (현재는 0이므로 연속된 1의 개수는 0)

•초기 조건:
•dp[0][0] = 0 (빈 문자열)
•나머지 dp[0][j]는 무한대로 설정.
•결과: min(dp[n][j]) for all j ≤ k.
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

int min_changes(const string &s, int k) {
    int n = s.length();
    // dp[i][j]: 첫 i글자까지 고려했을 때, 마지막에 연속된 1의 개수가 j인 상태에서의 최소 변경 횟수
    vector<vector<int>> dp(n + 1, vector<int>(k + 1, INT32_MAX));
    dp[0][0] = 0; // 초기 조건

    for(int i = 1; i <= n; i++) {
        for(int j = 0; j <= k; j++) {
            if(s[i-1] == '1') {
                if(j > 0 && dp[i-1][j-1] != INT32_MAX)
                    dp[i][j] = min(dp[i][j], dp[i-1][j-1]); // 그대로 1 유지
                if(dp[i-1][j] != INT32_MAX)
                    dp[i][j] = min(dp[i][j], dp[i-1][j] + 1); // 1을 0으로 변경
            }
            else { // s[i-1] == '0'
                // 0을 0으로 유지, 연속된 1의 개수는 0
                dp[i][0] = min(dp[i][0], dp[i-1][j]);
                // 0을 1로 변경, 연속된 1의 개수는 j+1
                if(j + 1 <= k && dp[i-1][j] != INT32_MAX)
                    dp[i][j+1] = min(dp[i][j+1], dp[i-1][j] + 1);
            }
        }
    }

    // 최종적으로 연속된 1의 개수가 최대 k인 모든 상태 중 최소 변경 횟수
    int res = INT32_MAX;
    for(int j = 0; j <= k; j++)
        res = min(res, dp[n][j]);

    return res;
}

int main() {
    string s = "1110111";
    int k = 2;
    cout << "최소 변경 횟수: " << min_changes(s, k) << endl;
    return 0;
}

시간 복잡도:

분석: 두 개의 중첩된 반복문 (문자열 길이 nk).

시간 복잡도: O(nk)

 

결론:

다이나믹 프로그래밍을 활용하여 이진 문자열에서 연속된 1의 개수를 제한하면서 최소 변경 횟수를 효율적으로 계산할 수 있습니다. 이 접근 방식은 상태 정의와 점화식을 통해 문제를 체계적으로 해결하며, 시간 복잡도는 O(nk)입니다.

 

'DataStructure > Algorithm' 카테고리의 다른 글

Dynamic Programming  (4) 2024.10.17
Bucket Sort (버킷 정렬)  (0) 2024.09.23
3-way QuickSort (3분할 퀵정렬)  (0) 2024.09.16
QuickSort (퀵정렬)  (0) 2024.09.16
MergeSort (병합 정렬)  (2) 2024.09.15