일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링부트핵심가이드
- 처음 만나는 AI 수학 with Python
- GIT
- d
- 네트워크 설정
- 코드로배우는스프링웹프로젝트
- network configuration
- baeldung
- 친절한SQL튜닝
- Kernighan의 C언어 프로그래밍
- 처음 만나는 AI수학 with Python
- 자료구조와 함께 배우는 알고리즘 입문
- iterator
- 데비안
- 목록처리
- 구멍가게코딩단
- 페이징
- 스프링 시큐리티
- /etc/network/interfaces
- 자바편
- 코드로배우는스프링부트웹프로젝트
- 선형대수
- 이터레이터
- 자료구조와함께배우는알고리즘입문
- 리눅스
- ㅒ
- 서버설정
- 알파회계
- 티스토리 쿠키 삭제
- resttemplate
- Today
- Total
bright jazz music
동적 프로그래밍(Dynamic Programming) 본문
동적 프로그래밍은 하나의 문제를 여러 개의 작은 문제로 나누어 해결하고 해당 결과를 저장한 후에 더 큰 문제를 해결할 때 사용하는 방법론이다. 따라서 알고리즘이라기보다는 문제 해결 방법론으로 보는 관점도 있다.
동적 프로그래밍은 개발자의 관점에서 볼 때, '큰 문제'에 중점을 두어야 한다. '큰 문제'가 있어야 동적 프로그래밍의 효율성이 극대화 된다.
즉, 동적계획법은 복잡한 문제를 더 작은 하위 문제로 나누어 해결하는 알고리즘 설계 기법이다.
특징:
- 작은 문제의 해결책을 저장(메모이제이션)하여 재사용
- 중복 계산을 피함으로써 성능 향상
- 상향식(bottom-up) 또는 하향식(top-down) 방식으로 구현 가능
피보나치 수열을 예로 들어 보면,
// 일반적인 재귀 방식 (비효율적): n이 커질수록 함수가 재귀적으로 수행되며 늘어나게 된다.
function fibRecursive(n) {
if (n <= 1) return n;
return fibRecursive(n-1) + fibRecursive(n-2);
}
// DP를 활용한 반복문 방식 (bottom-up)
function fibIterative(n) {
if (n <= 1) return n;
let dp = [0, 1];
for (let i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
// DP를 활용한 메모이제이션 방식 (top-down)
function fibDP(n, memo = {}) {
if (n <= 1) return n;
if (memo[n]) return memo[n];
memo[n] = fibDP(n-1, memo) + fibDP(n-2, memo);
return memo[n];
}
예제 : 문자열 일치
문자열이 일치하는지 판단할 때 주로 와일드카드 패턴이라는 것을 사용해 검증한다. 와일드카드 패턴 매칭은 문자열 매칭 문제의 한 종류로, 와일드카드 문자(*, ?등)을 포함한 패턴과 주어진 문자열을 비교하여 패턴과 일치하는지 확인하는 것이다. 와일드카드 문자를 말할 때 "*"만을 생각하는 경우가 많지만 "?"도 와일드카드 문자의 일종이다.
*: 0개 이상의 길이의 문자열과 일치할 때 사용
?: 하나의 문자와 일치할 때 사용
function isMatch(pattern, text) {
const m = pattern.length;
const n = text.length;
// dp[i][j]는 pattern의 처음 i 글자와 text의 처음 j글자가 일치하는지를 저장하는 배열
const dp = Array.from({length: m + 1}, () => Array(n + 1).fill(false));
// 빈 패턴과 빈 텍스트는 항상 일치
dp[0][0] = true;
// 패턴이 *로 시작하는 경우에는 빈 텍스트와도 일치하므로 dp[i][0]을 갱신
for(let i = 1; i <= m; i++) {
if(pattern[i - 1] === '*') { // [i - 1]로 수정 (원래 코드의 [i = 1] 오타 수정)
dp[i][0] = dp[i - 1][0];
}
}
// 동적 프로그래밍을 통해 dp 배열 갱신
for(let i = 1; i <= m; i++) {
for(let j = 1; j <= n; j++) {
if(pattern[i - 1] === '*') {
// 와일드카드인 경우
dp[i][j] = dp[i - 1][j] || dp[i][j - 1] || dp[i - 1][j - 1];
} else if (pattern[i - 1] === '?' || pattern[i - 1] === text[j - 1]) {
// ?이거나 현재 패턴 문자와 텍스트 문자가 일치하는 경우
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return dp[m][n];
}
// 테스트
console.log(isMatch("g*ks", "geeks")); // true
console.log(isMatch("ge?ks", "geeksforgeeks")); // true
console.log(isMatch("g*k", "gee")); // false
예제2 동전교환
/**
* 주어진 동전들을 사용하여 특정 금액을 만들 수 있는 최소 동전 개수를 계산하는 함수
* @param {number[]} coins - 사용 가능한 동전의 종류 배열
* @param {number} amount - 만들어야 하는 목표 금액
* @returns {number} - 최소 동전 개수 (불가능한 경우 -1 반환)
*/
function coinChange(coins, amount) {
// dp 배열 초기화: 각 인덱스는 해당 금액을 만들기 위한 최소 동전 개수를 저장
// 초기값으로 Infinity를 설정하여 아직 해당 금액을 만들 수 없음을 표시
let dp = new Array(amount + 1).fill(Infinity);
// 0원을 만드는데 필요한 동전 개수는 0개
dp[0] = 0;
// 각 동전에 대해 반복
for(let coin of coins) {
// 현재 동전부터 목표 금액까지 반복
for(let i = coin; i <= amount; i++) {
// 현재 금액을 만드는 최소 동전 개수는
// 이전에 계산된 값(dp[i])과
// 현재 동전을 사용했을 때의 값(dp[i - coin] + 1) 중 최소값
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
// 목표 금액을 만들 수 없는 경우(Infinity)는 -1 반환
// 가능한 경우는 해당 금액을 만드는데 필요한 최소 동전 개수 반환
return dp[amount] === Infinity ? -1 : dp[amount];
}
// 테스트 예시
console.log(coinChange([1, 2, 5], 11)); // 결과: 3 (5 + 5 + 1)
console.log(coinChange([2], 3)); // 결과: -1 (불가능)
console.log(coinChange([1, 2, 5], 15)); // 결과: 3 (5 + 5 + 5)
console.log(coinChange([1, 4, 5], 8)); // 결과: 2 (4 + 4)
// dp 배열의 변화 과정을 보여주는 예시 (amount = 5, coins = [1, 2, 5])
function showDpProcess(coins, amount) {
let dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
console.log("초기 dp 배열:", [...dp]);
for(let coin of coins) {
for(let i = coin; i <= amount; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
console.log(`${coin}원 동전 처리 후 dp 배열:`, [...dp]);
}
}
console.log("\nDP 배열 변화 과정:");
showDpProcess([1, 2, 5], 5);
/*
DP 배열 변화 과정:
초기 dp 배열: [ 0, Infinity, Infinity, Infinity, Infinity, Infinity ]
1원 동전 처리 후 dp 배열: [ 0, 1, 2, 3, 4, 5 ]
2원 동전 처리 후 dp 배열: [ 0, 1, 1, 2, 2, 3 ]
5원 동전 처리 후 dp 배열: [ 0, 1, 1, 2, 2, 1 ]
*/
예제3: 도둑질
/**
* 트리 구조의 마을에서 도둑질을 통해 얻을 수 있는 최대 골드를 계산하는 프로그램
* 인접한 집은 동시에 도둑질할 수 없다는 제약 조건이 있음
*/
// 입력값 정의
const N = 7; // 집의 개수
const gold = [6, 7, 4, 8, 2, 9, 5]; // 각 집에 보관된 골드의 양
// 트리 구조 정의 (인접 리스트 방식)
const tree = [
[1,2], // 집 0과 연결된 집들
[0, 3, 4], // 집 1과 연결된 집들
[0, 5, 6], // 집 2와 연결된 집들
[1], // 집 3과 연결된 집들
[1], // 집 4와 연결된 집들
[2], // 집 5와 연결된 집들
[2], // 집 6과 연결된 집들
];
/**
* DFS를 사용하여 각 노드에서의 최대 골드값을 계산하는 함수
* @param {number} node - 현재 처리중인 노드
* @param {number} parent - 부모 노드
* @param {Array<Array<number>>} tree - 트리 구조
* @param {Array<number>} gold - 각 집의 골드 양
* @param {Array<Array<number>>} dp - 동적 프로그래밍 배열
*/
function dfs(node, parent, tree, gold, dp) {
// dp[node][0]: 현재 노드를 선택하지 않았을 때의 최대 골드
// dp[node][1]: 현재 노드를 선택했을 때의 최대 골드
dp[node][0] = 0; // 현재 노드를 선택하지 않는 경우 초기화
dp[node][1] = gold[node]; // 현재 노드를 선택하는 경우 해당 노드의 골드값으로 초기화
// 현재 노드와 연결된 모든 이웃 노드에 대해 처리
for(let neighbor of tree[node]) {
// 부모 노드는 이미 처리했으므로 건너뛴다
if(neighbor === parent) continue;
// 이웃 노드에 대해 재귀적으로 DFS 수행
dfs(neighbor, node, tree, gold, dp);
// 현재 노드를 선택하지 않는 경우:
// 이웃 노드를 선택하거나 선택하지 않는 경우 중 큰 값을 더함
dp[node][0] += Math.max(dp[neighbor][0], dp[neighbor][1]);
// 현재 노드를 선택하는 경우:
// 이웃 노드는 선택할 수 없으므로 이웃 노드를 선택하지 않는 경우의 값을 더함
dp[node][1] += dp[neighbor][0];
}
}
/**
* 전체 트리에서 얻을 수 있는 최대 골드를 계산하는 함수
* @param {number} N - 집의 개수
* @param {Array<number>} gold - 각 집의 골드 양
* @param {Array<Array<number>>} tree - 트리 구조
* @returns {number} - 훔칠 수 있는 최대 골드량
*/
function getMaxGold(N, gold, tree) {
// dp[i][j]: i번 집에서 j상태(0:미선택, 1:선택)일 때의 최대 골드
const dp = Array.from(Array(N), () => Array(2).fill(0));
// 루트 노드(0번 집)부터 DFS 시작
dfs(0, -1, tree, gold, dp);
// 루트 노드를 선택한 경우와 선택하지 않은 경우 중 큰 값 반환
return Math.max(dp[0][0], dp[0][1]);
}
// 결과 출력
console.log(getMaxGold(N, gold, tree)); // 결과: 30
/**
* 예시 설명:
* 최대 골드 30을 얻는 방법:
* - 선택한 집들: [8(집3) + 9(집5) + 5(집6) + 2(집4) + 6(집0)] = 30
* - 인접한 집들은 선택되지 않았음
*/
동적프로그래밍은 크게 두 가지 특징을 가지고 있다.
1. 최적 부분 구조(Optimal Substructure)를 가진다.
전체를 한 번에 해결하기보다는 부분을 해결하는 최적해를 찾아내면 이를 사용해 전체를 해결하는 데 효율적일 수 있다.
2. 중복 부분 문제(Overlapping Subproblems)를 해결하는 데 유리하다.
전체 문제에서 일정 부분이 동일한 부분 문제로 구성돼 있다면 동적 프로그래밍은 훌륭한 접근법이 되 수 있다. 이 과정에서 동일한 부분 문제(또는 하위 문제)의 연산값을 재활용하여 성능을 높이는 전략을 사용할 수 있다.
위의 두 가지 상황에 해당할 수 있는 문제를 만났을 때 동적 프로그래밍을 사용하기에 적합하다고 할 수 있다. 피보나치 수열과 같은 최장 공통부분 수열(Longest Common Subsequence), 최단경로 문제 등이 동적 프로그래밍을 적용해 볼 만한 문제이다.
'Algorithm&Data structure > JS alg.' 카테고리의 다른 글
백트래킹 알고리즘(backtracking algorithms) (0) | 2024.11.11 |
---|---|
탐욕법 (greedy algorithm) (1) | 2024.11.09 |
최소신장트리(Minimum Spanning Tree, MST) + (프림, 크루스칼 알고리즘) (0) | 2024.11.05 |
너비우선탐색(Breadth-First Search, BFS) (0) | 2024.11.04 |
깊이우선탐색(DFS, Depth-First Search) (0) | 2024.11.03 |