답변:
동적 프로그래밍은 과거 지식을 사용하여 미래의 문제를보다 쉽게 해결하는 것입니다.
좋은 예는 n = 1,000,002의 피보나치 수열을 푸는 것입니다.
이 과정은 매우 오래 걸리지 만 n = 1,000,000 및 n = 1,000,001에 대한 결과를 제공하면 어떻게됩니까? 갑자기 문제가 더 관리하기 쉬워졌습니다.
동적 프로그래밍은 문자열 편집 문제와 같은 문자열 문제에서 많이 사용됩니다. 문제의 하위 집합을 해결 한 다음 해당 정보를 사용하여보다 어려운 원래 문제를 해결합니다.
동적 프로그래밍을 사용하면 일반적으로 결과를 일종의 테이블에 저장합니다. 문제에 대한 답이 필요할 때 표를 참조하여 이미 무엇인지 알고 있는지 확인하십시오. 그렇지 않은 경우 테이블의 데이터를 사용하여 답을 향한 디딤돌이됩니다.
Cormen Algorithms 책에는 동적 프로그래밍에 대한 훌륭한 장이 있습니다. 그리고 그것은 Google 도서에서 무료입니다! 여기서 확인 하십시오.
동적 프로그래밍은 재귀 알고리즘에서 동일한 하위 문제를 여러 번 계산하지 않도록하는 기술입니다.
하자가 피보나치 수의 간단한 예제를 가지고 : n 개의 찾는 일 에 의해 정의 된 피보나치 수를
F n = F n-1 + F n-2 및 F 0 = 0, F 1 = 1
이를 수행하는 확실한 방법은 재귀 적입니다.
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
주어진 피보나치 수는 여러 번 계산되므로 재귀는 불필요한 계산을 많이 수행합니다. 이를 개선하는 쉬운 방법은 결과를 캐시하는 것입니다.
cache = {}
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
if n in cache:
return cache[n]
cache[n] = fibonacci(n - 1) + fibonacci(n - 2)
return cache[n]
이를 수행하는 더 좋은 방법은 결과를 올바른 순서로 평가하여 재귀를 완전히 제거하는 것입니다.
cache = {}
def fibonacci(n):
cache[0] = 0
cache[1] = 1
for i in range(2, n + 1):
cache[i] = cache[i - 1] + cache[i - 2]
return cache[n]
일정한 공간을 사용하고 필요한 부분 결과 만 저장할 수 있습니다.
def fibonacci(n):
fi_minus_2 = 0
fi_minus_1 = 1
for i in range(2, n + 1):
fi = fi_minus_1 + fi_minus_2
fi_minus_1, fi_minus_2 = fi, fi_minus_1
return fi
동적 프로그래밍을 어떻게 적용합니까?
동적 프로그래밍은 일반적으로 문자열, 트리 또는 정수 시퀀스와 같이 고유 한 왼쪽에서 오른쪽 순서로 문제가있는 경우 작동합니다. 순진 재귀 알고리즘이 동일한 하위 문제를 여러 번 계산하지 않으면 동적 프로그래밍이 도움이되지 않습니다.
나는 논리를 이해하는 데 도움이되는 문제 모음을 만들었습니다 : https://github.com/tristanguigue/dynamic-programing
if n in cache
하향식 예에서와 같이 상향식에서 누락 된 조건이 있습니까? 아니면 뭔가 빠졌습니다.
Memoization은 함수 호출의 이전 결과를 저장할 때입니다 (실제 함수는 동일한 입력이 주어지면 항상 같은 것을 반환합니다). 결과가 저장되기 전에 알고리즘 복잡성에 차이가 없습니다.
재귀는 일반적으로 더 작은 데이터 집합으로 함수를 호출하는 메서드입니다. 대부분의 재귀 함수는 유사한 반복 함수로 변환 될 수 있기 때문에 알고리즘 복잡성에도 차이가 없습니다.
동적 프로그래밍은 해결하기 쉬운 하위 문제를 해결하고 그로부터 해답을 얻는 과정입니다. 대부분의 DP 알고리즘은 Greedy 알고리즘 (있는 경우)과 지수 (모든 가능성을 열거하고 최상의 알고리즘을 찾습니다) 사이의 실행 시간에 있습니다.
실행 시간을 줄이는 알고리즘 최적화입니다.
Greedy Algorithm은 일반적으로 동일한 데이터 세트에서 여러 번 실행될 수 있기 때문에 일반적으로 naive 라고하지만 Dynamic Programming은 최종 솔루션을 빌드하기 위해 저장해야하는 부분 결과에 대한 심층적 인 이해를 통해 이러한 함정을 피합니다.
간단한 예는 솔루션에 기여하는 노드를 통해서만 트리 또는 그래프를 순회하거나, 지금까지 찾은 솔루션을 테이블에 배치하여 동일한 노드를 순회하는 것을 피할 수 있습니다.
다음은 UVA의 온라인 판사 인 Edit Steps Ladder 의 동적 프로그래밍에 적합한 문제의 예입니다 .
Programming Challenges 책에서 발췌 한이 문제 분석의 중요한 부분을 간략히 설명하겠습니다. 확인해보십시오.
두 문자열이 얼마나 멀리 있는지 알려주는 비용 함수를 정의하면 그 문제를 잘 살펴보십시오.
대체- "shot"을 "spot"으로 변경하는 것과 같이 단일 문자를 패턴 "s"에서 텍스트 "t"의 다른 문자로 변경하십시오.
삽입- "ago"를 "agog"로 변경하는 것과 같이 텍스트 "t"와 일치하도록 패턴 "s"에 단일 문자를 삽입합니다.
삭제-패턴 "s"에서 단일 문자를 삭제하여 "hour"를 "our"로 변경하는 것과 같이 텍스트 "t"와 일치하도록하십시오.
이 각 작업을 한 단계 비용으로 설정하면 두 문자열 사이의 편집 거리를 정의합니다. 어떻게 계산합니까?
문자열의 마지막 문자가 일치, 대체, 삽입 또는 삭제되어야한다는 관찰을 사용하여 재귀 알고리즘을 정의 할 수 있습니다. 마지막 편집 작업에서 문자를 잘라 내면 한 쌍의 작업에서 한 쌍의 작은 문자열이 남습니다. i와 j를 각각 관련 접두사와 t의 마지막 문자라고합시다. 일치 / 대체, 삽입 또는 삭제 후 문자열에 해당하는 마지막 조작 후 3 개의 더 짧은 문자열 쌍이 있습니다. 세 쌍의 더 작은 문자열을 편집하는 비용을 알고 있다면 어떤 솔루션이 가장 적합한 솔루션인지 결정하고 그에 따라 해당 옵션을 선택할 수 있습니다. 우리는 재귀의 놀라운 것을 통해이 비용을 배울 수 있습니다.
#define MATCH 0 /* enumerated type symbol for match */ #define INSERT 1 /* enumerated type symbol for insert */ #define DELETE 2 /* enumerated type symbol for delete */ int string_compare(char *s, char *t, int i, int j) { int k; /* counter */ int opt[3]; /* cost of the three options */ int lowest_cost; /* lowest cost */ if (i == 0) return(j * indel(’ ’)); if (j == 0) return(i * indel(’ ’)); opt[MATCH] = string_compare(s,t,i-1,j-1) + match(s[i],t[j]); opt[INSERT] = string_compare(s,t,i,j-1) + indel(t[j]); opt[DELETE] = string_compare(s,t,i-1,j) + indel(s[i]); lowest_cost = opt[MATCH]; for (k=INSERT; k<=DELETE; k++) if (opt[k] < lowest_cost) lowest_cost = opt[k]; return( lowest_cost ); }
이 알고리즘은 정확하지만 속도가 느립니다.
우리 컴퓨터에서 실행하면 두 개의 11 문자 문자열을 비교하는 데 몇 초가 걸리며 계산은 더 이상 더 이상 이루어지지 않습니다.
알고리즘이 왜 그렇게 느린가요? 값을 반복해서 다시 계산하기 때문에 지수 시간이 걸립니다. 문자열의 모든 위치에서 재귀는 3 가지 방식으로 분기되는데, 이는 적어도 3 ^ n의 속도로 증가한다는 것을 의미합니다. 실제로 대부분의 통화는 두 지수 중 하나만 감소시키기 때문에 두 지수 중 하나만 감소하기 때문에 훨씬 빠릅니다.
그렇다면 어떻게 알고리즘을 실용적으로 만들 수 있을까요? 중요한 재귀 호출은 대부분 이전에 이미 계산 된 컴퓨팅 작업이라는 것입니다. 우리가 어떻게 알아? 글쎄, | s | 만있을 수 있습니다 · | t | 재귀 호출의 매개 변수로 사용할 고유 한 (i, j) 쌍이 많기 때문에 가능한 고유 재귀 호출입니다.
이들 (i, j) 쌍 각각에 대한 값을 테이블에 저장함으로써, 재 계산을 피하고 필요에 따라 찾아 볼 수 있습니다.
표는 2 차원 행렬 m이며, 여기서 각각의 | s | · | t | 셀에는이 하위 문제에 대한 최적의 솔루션 비용과이 위치에 도달 한 방법을 설명하는 부모 포인터가 포함됩니다.
typedef struct { int cost; /* cost of reaching this cell */ int parent; /* parent cell */ } cell; cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */
동적 프로그래밍 버전은 재귀 버전과 세 가지 차이점이 있습니다.
먼저 재귀 호출 대신 테이블 조회를 사용하여 중간 값을 가져옵니다.
** 두 번째 **는 각 셀의 부모 필드를 업데이트하므로 나중에 편집 순서를 재구성 할 수 있습니다.
** 세 번째, ** 세 번째는
cell()
m [| s |] [| t |] .cost를 반환하는 대신 보다 일반적인 목표 함수를 사용하여 계측됩니다 . 이를 통해 우리는이 루틴을 광범위한 문제에 적용 할 수 있습니다.
여기에서 가장 최적의 부분 결과를 수집하는 데 필요한 매우 구체적인 분석은 솔루션을 "동적"으로 만드는 것입니다.
다음 은 동일한 문제에 대한 대체 솔루션입니다. 실행이 다르더라도 "동적"입니다. 솔루션을 UVA 온라인 판사에게 제출하여 솔루션의 효율성을 확인하십시오. 나는 그러한 무거운 문제가 어떻게 그렇게 효율적으로 해결되었는지 놀랍습니다.
동적 프로그래밍의 핵심 부분은 "중복 하위 문제"및 "최적 하위 구조"입니다. 문제의 이러한 특성은 최적의 솔루션이 하위 문제에 대한 최적의 솔루션으로 구성됨을 의미합니다. 예를 들어 최단 경로 문제는 최적의 하위 구조를 나타냅니다. A에서 C까지의 최단 경로는 A에서 일부 노드 B까지의 최단 경로에 이어 해당 노드 B에서 C까지의 최단 경로입니다.
가장 짧은 경로 문제를 해결하려면 다음을 수행하십시오.
우리는 상향식으로 작업하고 있기 때문에 하위 문제를 해결하는 데 도움이 될 때까지 하위 문제에 대한 해결책을 이미 가지고 있습니다.
동적 프로그래밍 문제에는 하위 문제가 겹치거나 최적의 하위 구조가 있어야합니다. 피보나치 시퀀스 생성은 동적 프로그래밍 문제가 아닙니다. 하위 문제가 겹치기 때문에 메모를 사용하지만 최적화 문제가 없기 때문에 최적의 하위 구조가 없습니다.
다이나믹 프로그래밍
정의
동적 프로그래밍 (DP)은 하위 문제가 겹치는 문제를 해결하기위한 일반적인 알고리즘 설계 기술입니다. 이 기술은 1950 년대 미국의 수학자 "Richard Bellman"에 의해 발명되었습니다.
핵심 아이디어
핵심 아이디어는 재 계산을 피하기 위해 작은 하위 문제가 겹치는 것에 대한 답변을 저장하는 것입니다.
동적 프로그래밍 속성
또한 동적 프로그래밍 (특정 유형의 문제에 대한 강력한 알고리즘)을 처음 접했습니다.
가장 간단한 말로, 동적 프로그래밍을 이전 지식 을 사용하여 재귀 적 접근으로 생각하십시오.
이전 지식 이 가장 중요합니다. 이미 가지고있는 하위 문제의 해결 방법을 추적하십시오.
Wikipedia의 dp에 대한 가장 기본적인 예를 고려하십시오.
피보나치 수열 찾기
function fib(n) // naive implementation
if n <=1 return n
return fib(n − 1) + fib(n − 2)
n = 5로 함수 호출을 분류하자
fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
특히 fib (2)는 처음부터 세 번 계산되었습니다. 더 큰 예에서, 더 많은 fib 값 또는 하위 문제가 재 계산되어 지수 시간 알고리즘으로 이어집니다.
이제 데이터 구조에서 이미 찾은 값을 Map 이라고 저장하여 사용해보십시오.
var m := map(0 → 0, 1 → 1)
function fib(n)
if key n is not in map m
m[n] := fib(n − 1) + fib(n − 2)
return m[n]
하위 문제의 솔루션을 아직지도에 저장하지 않은 경우지도에 저장합니다. 우리가 이미 계산 한 값을 저장하는이 기술을 메모라고합니다.
마지막으로 문제의 경우 먼저 상태를 찾아보십시오 (가능한 하위 문제 및 이전 하위 문제의 솔루션을 다른 하위 문제에 사용할 수 있도록 더 나은 재귀 접근 방식을 생각해보십시오).
동적 프로그래밍은 하위 문제가 겹치는 문제를 해결하는 기술입니다. 동적 프로그래밍 알고리즘은 모든 하위 문제를 한 번만 해결 한 다음 답변을 테이블 (어레이)에 저장합니다. 하위 문제가 발생할 때마다 답변을 다시 계산하는 작업을 피하십시오. 동적 프로그래밍의 기본 개념은 다음과 같습니다. 일반적으로 하위 문제의 알려진 결과 테이블을 유지하여 동일한 항목을 두 번 계산하지 마십시오.
동적 프로그래밍 알고리즘 개발의 7 단계는 다음과 같습니다.
6. Convert the memoized recursive algorithm into iterative algorithm
필수 단계는? 이것은 최종 형태가 비 재귀 적이라는 것을 의미합니까?
요컨대 재귀 메모와 동적 프로그래밍의 차이점
이름에서 알 수 있듯이 동적 프로그래밍은 이전에 계산 된 값을 사용하여 다음 새 솔루션을 동적으로 구성합니다.
동적 프로그래밍 적용 위치 : 솔루션이 최적의 하위 구조와 겹치는 하위 문제를 기반으로하는 경우 이전 계산 된 값을 사용하면 유용하므로 다시 계산할 필요가 없습니다. 상향식 접근입니다. 이 경우 fib (n)을 계산해야한다고 가정하면 이전에 계산 된 fib (n-1) 및 fib (n-2) 값을 추가하기 만하면됩니다.
재귀 : 기본적으로 문제를 더 작은 부분으로 세분하여 쉽게 해결할 수 있지만 이전에 다른 재귀 호출에서 동일한 값을 계산 한 경우 재 계산을 피할 수는 없습니다.
메모 : 기본적으로 이전 계산 된 재귀 값을 테이블에 저장하는 것을 메모라고합니다. 이는 일부 이전 호출로 이미 계산 된 경우 재 계산을 피하므로 모든 값이 한 번 계산됩니다. 따라서 계산하기 전에이 값이 이미 계산되었는지 확인하고 이미 계산 된 경우 다시 계산하는 대신 테이블에서 동일한 값을 반환합니다. 또한 하향식 접근 방식입니다
다음의 간단한 파이썬 코드 예는 Recursive
, Top-down
, Bottom-up
피보나치 시리즈에 대한 접근 방식 :
def fib_recursive(n):
if n == 1 or n == 2:
return 1
else:
return fib_recursive(n-1) + fib_recursive(n-2)
print(fib_recursive(40))
def fib_memoize_or_top_down(n, mem):
if mem[n] is not 0:
return mem[n]
else:
mem[n] = fib_memoize_or_top_down(n-1, mem) + fib_memoize_or_top_down(n-2, mem)
return mem[n]
n = 40
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
print(fib_memoize_or_top_down(n, mem))
def fib_bottom_up(n):
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
if n == 1 or n == 2:
return 1
for i in range(3, n+1):
mem[i] = mem[i-1] + mem[i-2]
return mem[n]
print(fib_bottom_up(40))