방송대/알고리즘
11강. 동적 프로그래밍
monimoni
2025. 5. 16. 20:57
1. 동적 프로그래밍
- 문제의 크기가 작은 소문제에 대한 해를 테이블에 저장해 놓고, 이를 이용해서 크기가 보다 큰 문제의 해를 점진적으로 만들어가는 상향식 접근 방법
- 각 소문제는 원래 주어진 문제와 동일한 문제 ➝ 입력 크기만 작아진 문제
- 소문제들은 서로 독립일 필요는 없음
- 동적 “프로그래밍” ➝ “동적 계획법”
- 컴퓨터 프로그램과는 무관, 해를 구축하는 데 테이블을 이용한다는 의미
- 최소값 / 최댓값을 구하는 최적화 문제에 주로 사용
- "최적성의 원리 ( principle of optimality )"를 반드시 만족하는 문제에만 적용 가능
- 최적성의 원리란?
- 주어진 문제에 대한 최적해는 주어진 문제의 소문제에 대한 최적해로 구성
- 동적 프로그래밍 방법의 처리 과정
- (0) 최적성의 원리가 주어진 문제에 성립하는 지 확인
- (1) 주어진 문제에 대해서 최적해를 제공하는 점화식 도출
- (2) 가장 작은 소문제부터 점화식의 해를 구한 뒤 이를 테이블에 저장
- (3) 테이블에 저장된 소문제의 해를 이용하여 점차적으로 입력의 크기가 큰 상위 문제의 해를 구해 나감
2. 피보나치 수열
- 정의
- f(n) = f(n-1) + f(n-2) (n ≥ 2)
- f(n) = 1 (n = 1)
- f(n) = 0 (n = 0)
- 알고리즘
Fibo(n) {
f[0] = 0;
f[1] = 1;
for (i = 2; i <= n; i++)
f[i] = f[i-1] + f[i-2];
return f[n];
}
- 성능 : O(n)
3. 분할정복 방법을 적용한 피보나치 수열
f(n) {
if (n <= 0) return (0);
if (n == 1) return (1);
else return( f(n-1) + f(n-2) );
}
- 소문제가 독립이 아니므로 중복된 계산이 필요 ➝ 매우 비효율적
- 그렇기에 동적 프로그래밍 방법을 적용해야 함
4. 행렬의 연쇄적 곱셈
- n개의 행렬 (M₁, M₂, ⋯, Mₙ)을 연쇄적으로 곱하는 경우
- 결합법칙 성립
- M₁ · M₂ · M₃ ➝ ((M₁ · M₂) · M₃) = (M₁ · (M₂ · M₃))
- 여러 가지 다른 곱셈 순서가 존재 ➝ 곱셈의 횟수가 달라짐
- 결합법칙 성립
- 행렬의 연쇄적 곱셈 문제란?
- n개의 행렬을 연쇄적으로 곱할 때 최소의 기본 곱셈 횟수를 갖는 행렬의 곱셈 순서를 구하는 문제
- 기본 곱셈 횟수를 최소화할 수 있는 방법을 찾는 것
- n개의 행렬을 연쇄적으로 곱할 때 최소의 기본 곱셈 횟수를 갖는 행렬의 곱셈 순서를 구하는 문제
- 기본 곱셈
- 행렬 원소끼리의 곱셈
- Mᵢ : m×n
- Mⱼ : n×r
- Mᵢ × Mⱼ → 결과: m×r
- 기본 곱셈 횟수 = m × n × r
- 행렬 원소끼리의 곱셈
5. 최적성의 원리
- n개의 행렬을 곱하는 최적의 순서는 n개 행렬의 연쇄적 곱셈 중 일부 부분에서도 최적의 곱셈 순서를 포함함
- 예시
- 7개 행렬을 곱하는 최적의 순서 : (M₁M₂)((((M₃M₄)M₅)M₆)M₇)
- 그 중 일부인 M₃, M₄, M₅를 곱하는 최적의 순서 : ((M₃M₄)M₅)
- 부분 문제들의 최적해로 n개 행렬을 곱하는 최적의 순서를 구할 수 있음
- → 최적성의 원리 성립
- → 동적 프로그래밍 방법으로 해결 가능
6. 연쇄 행렬 곱셈 문제의 점화식
- n개의 행렬 Mᵢ ( dᵢ₋₁ × dᵢ 차원 ) ( 1 ≤ i ≤ n )
- M₁ × M₂ × M₃ × ... × Mₙ₋₁ × Mₙ d₀×d₁ d₁×d₂ d₂×d₃ dₙ₋₁×dₙ
- 최적해를 저장하는 테이블 : C(i, j) ( 1 ≤ i ≤ j ≤ n )
- Mᵢ × Mᵢ₊₁ × ... × Mⱼ 을 수행하는 데 필요한 기본 곱셈의 최소 횟수
- 최종 목표 : C(1, n) = ?
- 초기 조건: C(i, i) = 0
- C(i, j) = minᵢ≤k<j { C(i, k) + C(k+1, j) + dᵢ₋₁ * dₖ * dⱼ } ( i < j )
- dᵢ₋₁ * dₖ * dⱼ( 결합 비용 )은 Mᵢ~Mₖ과 Mₖ₊₁~Mⱼ를 곱할 때의 연산 수
- C(i, k)는 Mᵢ x … xMₖ
- C(k+1, j)는 Mₖ₊₁~Mⱼ
- 가장 적은 비용이 나오는 k를 저장 : P(i, j) = k
7. 행렬의 연쇄적 곱셈 알고리즘
MinMatMult(n, d[ ]) {
int i, j, k, s;
int C[n+1][n+1];
for (i = 1; i <= n; i++)
C[i][i] = 0;
for (s = 1; s <= n - 1; s++) {
for (i = 1; i <= n - s; i++) {
j = i + s;
C[i][j] = minᵢ≤k<j ( C[i][k] + C[k+1][j] + d[i-1] * d[k] * d[j] );
P[i][j] = 최소값이 되는 k의 값;
}
}
return (C[1][n], P[1..n][1..n]);
}
- 입력
- n: 행렬의 개수
- d[0..n]: 각 행렬의 크기 (i번째 행렬의 크기는 d[i-1] × d[i])
- 출력
- C[1][n]: n개의 행렬을 곱하는 데 필요한 기본 곱셈 횟수의 최소값
- P[1..n][1..n]: 최적의 곱셈 순서를 구할 수 있는 분할 위치 정보 배열
8. 행렬의 연쇄적 곱셈 알고리즘 성능과 특징
- 시간 복잡도 → O(n³)
- 최적의 행렬 곱셈 순서를 구하는 알고리즘 → O(n)
- P[i][j]를 이용할 때마다 나눌 부분이 하나씩 줄어듦
- n개 행렬의 곱셈 순서에서는 n - 2번의 분할 지점이 필요 → O(n)
- P[i][j]를 이용할 때마다 나눌 부분이 하나씩 줄어듦
9. 최장 공통 부분 수열
- LCS, Longest Common Subsequence
- 두 스트링의 공통된 부분 수열 중 가장 긴 수열을 구하는 것
- 부분 수열 : 문자열( 스트링 )에서 연속일 필요는 없지만, 순서는 유지되는 문자열( 스트링 )의 일부분
- ex. "KNOU" → 부분 수열 : O, KO, KU, NO, KOU, KNOU …
- 부분 수열 : 문자열( 스트링 )에서 연속일 필요는 없지만, 순서는 유지되는 문자열( 스트링 )의 일부분
- X = x₁x₂⋯xₙ 과 Y = y₁y₂⋯yₘ 사이의 공통 부분 수열 l₁l₂⋯lₛ
- 1 ≤ k ≤ s 인 모든 lₖ가 스트링 X와 Y에 존재
- k가 커질수록 X와 Y에 존재하는 위치도 커짐( 뒤쪽에 위치 )
10. LCS에서의 최적성의 원리
- 두 스트링 X와 Y 사이의 LCS는 이들의 어떤 서브스트링 사이의 LCS를 포함
- (1) X의 마지막 글자가 Y의 마지막 글자와 같은 경우
- x₁⋯xₙ₋₁ 과 y₁⋯yₘ₋₁ 의 LCS에 마지막 글자를 추가한 경우
- (2) X의 마지막 글자가 Y의 마지막 글자와 다르며 LCS에 사용되지 않은 경우
- x₁⋯xₙ₋₁ 과 y₁⋯yₘ 의 LCS와 동일
- (3) X의 마지막 글자가 Y의 마지막 글자와 다르며 LCS에 사용된 경우 (이 경우 X의 마지막 글자는 Y의 마지막 글자 이전의 다른 글자와 일치)
- x₁⋯xₙ 과 y₁⋯yₘ₋₁ 의 LCS와 동일
- (1) X의 마지막 글자가 Y의 마지막 글자와 같은 경우
11. LCS의 점화식
- 𝑋 = x₁x₂⋯xₙ 과 𝑌 = y₁y₂⋯yₘ 간의 LCS 점화식
- LCS(i, j) = ε( 빈 스트링 ) ( i = 0 또는 j = 0인 경우 )
- LCS(i, j) = LCS(i−1, j−1) + xᵢ ( xᵢ = yⱼ 인 경우 )
- LCS(i, j) = max{ LCS(i, j−1), LCS(i−1, j) } ( xᵢ ≠ yⱼ 인 경우 )
- 최종목표 : LCS(n, m) = ?
12. LCS 알고리즘 – LCS 길이와 관련 정보
LCS(n, X[ ], m, Y[ ]){
int LCS[n+1][m+1]; // LCS 길이 테이블
int i, j;
LCS[0][0] = 0;
for (i = 1; i <= n; i++)
LCS[i][0] = 0; // 첫 열의 초기화
for (j = 1; j <= m; j++)
LCS[0][j] = 0; // 첫 행의 초기화
for (i = 1; i <= n; i++)
for (j = 1; j <= m; j++)
if (X[i] == Y[j])
LCS[i][j] = LCS[i-1][j-1] + 1;
else
LCS[i][j] = max(LCS[i-1][j], LCS[i][j-1]);
return (LCS[n][m], LCS[0..n][0..m]);
}
- 입력
- X[1..n], Y[1..m] : 두 문자열
- n, m: 문자열 길이
- 출력
- LCS[n][m]: LCS의 길이
- LCS[0..n][0..m]: LCS 길이 테이블 전체 ( 문자열 재구성에 사용 가능 )
13. LCS 알고리즘 – LCS 출력
GetLCS(n, X, m, Y, LCS[ ][ ]) {
int i = n, j = m, idx = LCS[n][m];
char L[idx + 1]; // LCS를 저장할 배열
while (idx > 0) // LCS 길이만큼 공통 문자 찾을 때까지 반복
{
if (X[i] == Y[j]) {
L[idx] = X[i]; // 공통 문자를 LCS로 추가
idx--; i--; j--;
}
else if (LCS[i][j - 1] > LCS[i - 1][j])
j--; // LCS[i][j]와 일치하는 큰 값 쪽으로 j를 1 줄임
else
i--; // 또는 i를 1 줄임
}
return L[1..LCS[n][m]];
}
- 입력
- X[1..n], Y[1..m]: 두 문자열
- LCS[0..n][0..m]: LCS 길이 테이블
- 출력
- L[1..LCS[n][m]]: 최장 공통 부분 수열 (LCS) 문자열
14. LCS 알고리즘 성능 및 특징
- 길이 n과 m인 두 스트링의 LCS 길이 → O(nm)
- 각각 길이 n과 m을 사용하는 중첩된 이중 루프
- 테이블 LCS[ ][ ]로부터 LCS를 구하는 알고리즘 → O(n + m)
- idx가 LCS 길이만큼 줄어들 때까지 반복하는 형태의 루프로 구성
- 실제로는 i = n, j = m부터 최대 i = 0, j = 0까지 도달 가능
- idx가 LCS 길이만큼 줄어들 때까지 반복하는 형태의 루프로 구성
15. 정리하기
- 기본 개념
- 상향식 접근 방법, 소문제는 서로 독립일 필요 없음 (중첩 가능)
- 최적성의 원리, 적용 단계, 피보나치 수열
- 행렬의 연쇄적 곱셈
- n개 행렬의 연쇄적 곱셈에서 최소의 기본 곱셈 횟수를 가진 곱셈 순서를 구하는 문제
- C(i, j) = minᵢ≤k<j { C(i, k) + C(k+1, j) + dᵢ₋₁ × dₖ × dⱼ } (i < j)
- 시간 복잡도 : O(n³)
- 최적의 곱셈 순서는 P(i, j) = k 정보를 활용해서 구함
- 최장 공통 부분 수열
- 두 스트링의 공통된 부분 수열 중 가장 긴 수열을 구하는 문제
- LCS(i, j) = ε( 빈 스트링 ) (i = 0 또는 j = 0인 경우)
- LCS(i, j) = LCS(i−1, j−1) + xᵢ (xᵢ = yⱼ 인 경우)
- LCS(i, j) = max{ LCS(i, j−1), LCS(i−1, j) } (xᵢ ≠ yⱼ 인 경우)
- 시간 복잡도: O(nm) (n = |X|, m = |Y|)
728x90