답변:
rev4 : 사용자 Sammaron의 매우 설득력있는 설명은 아마도이 대답이 이전에 하향식과 상향식을 혼동한다고 언급했습니다. 원래이 답변 (rev3)과 다른 답변에서 "하단은 메모 화"( "하위 문제 가정")라고 말했지만, 그 반대 일 수 있습니다 (즉, "하향 문제"는 "하위 문제 가정"및 " 상향식 "은"하위 문제 구성 "일 수 있습니다. 이전에는 동적 프로그래밍의 하위 유형이 아닌 다른 종류의 동적 프로그래밍 인 메모에 대해 읽었습니다. 나는 그것을 구독하지 않았음에도 불구하고 그 견해를 인용했다. 나는이 답변이 문헌에서 적절한 참조를 찾을 수있을 때까지 용어에 대해 불가지론 적으로 다시 작성했다. 또한이 답변을 커뮤니티 위키로 변환했습니다. 학술 자료를 선호하십시오. 레퍼런스 목록:} {문학 : 5 }
동적 프로그래밍은 중복 작업을 다시 계산하지 않는 방식으로 계산 순서를 정하는 것입니다. 주요 문제 (하위 문제 트리의 루트)와 하위 문제 (하위 트리)가 있습니다. 하위 문제는 일반적으로 반복되고 겹칩니다 .
예를 들어, 좋아하는 Fibonnaci의 예를 고려하십시오. 순진한 재귀 호출을 한 경우 이것은 하위 문제의 전체 트리입니다.
TOP of the tree
fib(4)
fib(3)...................... + fib(2)
fib(2)......... + fib(1) fib(1)........... + fib(0)
fib(1) + fib(0) fib(1) fib(1) fib(0)
fib(1) fib(0)
BOTTOM of the tree
(다른 드문 문제에서이 트리는 일부 분기에서 무한 종료되어 비 종료를 나타낼 수 있으므로 트리의 맨 아래가 무한대로 커질 수 있습니다. 또한 일부 문제에서는 전체 트리가 어떻게 보이는지 알 수 없습니다. 따라서 공개 할 하위 문제를 결정하기위한 전략 / 알고리즘이 필요할 수 있습니다.)
상호 배타적이지 않은 동적 프로그래밍에는 적어도 두 가지 주요 기술이 있습니다.
메모 화-이것은 laissez-faire 접근법입니다. 이미 모든 하위 문제를 계산했으며 최적의 평가 순서가 무엇인지 모를 것으로 가정합니다. 일반적으로 루트에서 재귀 호출 (또는 반복되는 동등 항목)을 수행하고 최적의 평가 순서에 가까워 지거나 최적의 평가 순서에 도달하는 데 도움이된다는 증거를 얻습니다. 결과 를 캐시 하므로 재귀 호출이 하위 문제를 다시 계산하지 않도록 하여 중복 하위 트리가 다시 계산되지 않도록해야합니다.
fib(100)
이를 fib(100)=fib(99)+fib(98)
호출 fib(99)=fib(98)+fib(97)
하면,, ... 등 ..., fib(2)=fib(1)+fib(0)=1+0=1
. 그런 다음 마침내 해결 fib(3)=fib(2)+fib(1)
되지만 fib(2)
캐시했기 때문에 다시 계산할 필요가 없습니다 .도표-동적 프로그래밍을 "표 작성"알고리즘으로 생각할 수도 있습니다 (일반적으로 다차원이지만이 '표'는 매우 드문 경우에 비 유클리드 기하학을 가질 수 있습니다 *). 이것은 메모 작성과 비슷하지만 한 단계 더 진행됩니다. 계산을 수행 할 정확한 순서를 미리 선택해야합니다. 이것은 순서가 정적 인 것이 아니라 메모보다 훨씬 더 유연하다는 것을 의미하지는 않습니다.
fib(2)
, fib(3)
, fib(4)
... 더 쉽게 다음 사람을 계산할 수 있도록 모든 값을 캐싱. 또한 테이블을 채우는 것으로 생각할 수도 있습니다 (다른 형태의 캐싱).그것은 "동적 프로그래밍"패러다임에서, 가장 일반적인의에서 (나는 프로그래머가 전체 트리를 고려 말할 것이다 다음원하는 모든 속성 (일반적으로 시간 복잡성 및 공간 복잡성 조합)을 최적화 할 수있는 하위 문제 평가 전략을 구현하는 알고리즘을 작성합니다. 전략은 특정 하위 문제와 함께 어딘가에서 시작해야하며 해당 평가 결과에 따라 자체적으로 적용될 수 있습니다. "동적 프로그래밍"의 일반적인 의미에서 이러한 하위 문제를 캐시하려고 시도 할 수 있으며,보다 일반적으로 다양한 데이터 구조의 그래프와 같이 미묘한 차이로 하위 문제를 다시 방문하지 않도록하십시오. 종종 이러한 데이터 구조는 배열이나 테이블과 같은 핵심 요소입니다. 더 이상 필요하지 않은 하위 문제에 대한 솔루션을 버릴 수 있습니다.)
[이전에,이 답변은 하향식 용어와 상향식 용어에 대한 진술을했다; 메모와 표라고하는 두 가지 주요 접근 방식이 있습니다 (모두는 아니지만). 대부분의 사람들이 사용하는 일반적인 용어는 여전히 "동적 프로그래밍"이고 일부 사람들은 "동적 프로그래밍"의 특정 하위 유형을 지칭하기 위해 "Memoization"이라고 말합니다. 이 답변은 커뮤니티가 학술 논문에서 적절한 참조를 찾을 때까지 하향식과 상향식 중 어느 것을 말하는지는 거부합니다. 궁극적으로 용어보다는 구별을 이해하는 것이 중요합니다.]
Memoization은 코딩이 매우 쉽고 (일반적으로 * 자동으로이를 수행하는 "memoizer"주석 또는 래퍼 함수를 작성할 수 있음) 첫 번째 접근 방식이되어야합니다. 표의 단점은 주문을해야한다는 것입니다.
누군가가 이미 컴파일 쓴 경우, 예를 들어 ... 함수를 직접 작성, 및 / 또는 불순한 / 비 기능 프로그래밍 언어로 코딩하는 경우 * (이것은 단지 쉽게 실제로 fib
기능을 반드시 자체에 대한 재귀 호출을하고, 재귀 호출이 새로운 메모 함수를 호출하는지 확인하지 않고 함수를 마술로 메모 할 수는 없습니다.
하향식과 상향식은 모두 자연스럽지 않지만 재귀 또는 반복 테이블 채우기로 구현할 수 있습니다.
메모를 사용하면 트리가 매우 깊을 경우 (예 :) fib(10^6)
지연된 각 계산을 스택에 배치해야하고 스택 중 10 ^ 6을 갖기 때문에 스택 공간이 부족합니다.
하위 문제를 방문하거나 방문하려는 순서가 최적이 아닌 경우 특히 하위 문제를 계산하는 방법이 두 가지 이상인 경우 (일반적으로 캐싱이 문제를 해결할 수 있지만 이론적으로는 캐싱이 가능할 수 있습니다) 이국적인 경우는 아닙니다). Memoization은 일반적으로 시간 복잡성을 공간 복잡성에 추가합니다 (예 : Fib로 테이블을 사용하면 O (1) 공간을 사용할 수 있지만 Fib를 사용한 메모는 O (N)을 사용하는 것처럼 계산을 버릴 수있는 자유가 더 많습니다. 스택 공간).
또한 매우 복잡한 문제를 겪고 있다면 표를 작성하는 것 외에는 선택의 여지가 없을 수도 있습니다 (또는 메모를 원하는 곳에서 조정하는 데 더 적극적인 역할을 수행해야 함). 또한 최적화가 절대적으로 중요하고 최적화해야하는 상황에있는 경우 표를 사용하면 메모를 통해 제정신이되지 않는 최적화를 수행 할 수 있습니다. 겸손한 의견으로는 일반적인 소프트웨어 엔지니어링에서는이 두 경우 중 어느 것도 나오지 않기 때문에 (스택 공간과 같은) 무언가가 테이블을 만들 필요가 없다면 메모 ( "답을 캐시하는 기능")를 사용하는 것입니다. 기술적으로 스택 분출을 피하기 위해 1) 스택 크기 제한을 허용하는 언어로 스택 크기 제한을 늘리거나 2) 스택을 가상화하기 위해 지속적인 추가 작업을 수행 할 수 있습니다 (ick).
여기에서는 일반적인 DP 문제 일뿐만 아니라 메모와 표를 흥미롭게 구분하는 특별한 관심사를 보여줍니다. 예를 들어, 한 공식이 다른 공식보다 훨씬 쉬워 지거나 기본적으로 테이블이 필요한 최적화가있을 수 있습니다.
python memoization decorator
; 일부 언어에서는 메모 패턴을 캡슐화하는 매크로 나 코드를 작성할 수 있습니다. memoization 패턴은 "함수를 호출하는 것보다 캐시에서 값을 찾는 것 (값이 없으면 계산하여 캐시에 먼저 추가)"에 지나지 않습니다.
fib(513)
. 내가 느끼는 과부하 용어는 여기에 있습니다. 1) 더 이상 필요없는 하위 문제를 항상 버릴 수 있습니다. 2) 불필요하게 하위 문제를 계산하지 않아도됩니다. 3) 1과 2는 명시적인 데이터 구조가 없으면 하위 문제를 저장하기 위해 코딩하기가 훨씬 어려울 수 있습니다.
하향식 DP와 상향식 DP는 동일한 문제를 해결하는 두 가지 방법입니다. 피보나치 수 계산에 대한 메모 화 된 (위에서 아래로) vs 동적 (아래에서 위로) 프로그래밍 솔루션을 고려하십시오.
fib_cache = {}
def memo_fib(n):
global fib_cache
if n == 0 or n == 1:
return 1
if n in fib_cache:
return fib_cache[n]
ret = memo_fib(n - 1) + memo_fib(n - 2)
fib_cache[n] = ret
return ret
def dp_fib(n):
partial_answers = [1, 1]
while len(partial_answers) <= n:
partial_answers.append(partial_answers[-1] + partial_answers[-2])
return partial_answers[n]
print memo_fib(5), dp_fib(5)
개인적으로 메모가 훨씬 더 자연 스럽습니다. 재귀 함수를 사용하고 기계적인 프로세스로 캐시 할 수 있습니다 (캐시에서 먼저 조회 응답을 반환하고 가능하면 반환하십시오. 그렇지 않으면 재귀 적으로 계산 한 다음 반환하기 전에 나중에 사용할 수 있도록 캐시에 계산을 저장하십시오). 동적 프로그래밍을 위해서는 솔루션이 계산되는 순서를 인코딩해야합니다. 따라서 작은 문제가 발생하기 전에 "큰 문제"가 계산되지 않습니다.
동적 프로그래밍의 주요 특징은 중복되는 하위 문제가 있다는 것 입니다. 즉, 해결하려는 문제가 하위 문제로 분류 될 수 있으며 이러한 하위 문제 중 많은 부분이 하위 하위 문제를 공유합니다. 그것은 "분열과 정복"과 같지만 여러 번 같은 일을하게됩니다. 2003 년부터 이러한 문제를 가르치거나 설명 할 때 사용한 예 : 피보나치 수를 재귀 적으로 계산할 수 있습니다 .
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
자주 사용하는 언어를 사용하여 실행 해보십시오 fib(50)
. 매우 오랜 시간이 걸립니다. fib(50)
그 자체 만큼이나 많은 시간 ! 그러나 많은 불필요한 작업이 수행되고 있습니다. fib(50)
호출 fib(49)
하고 fib(48)
, 그러나 그 모두는 호출 끝날 fib(47)
값이 동일 할지라도. 사실, fib(47)
세 번 계산됩니다에서 직접 호출에 의해 fib(49)
, 직접 호출에서 fib(48)
, 또한 서로 직접 호출에 의해 fib(48)
,의 계산에 의해 산란 된 사람은 fib(49)
당신이 볼 그래서 ... 우리는이 중복 하위 문제를 .
좋은 소식 : 같은 값을 여러 번 계산할 필요가 없습니다. 한 번 계산하면 결과를 캐시하고 다음에 캐시 된 값을 사용하십시오! 이것이 동적 프로그래밍의 본질입니다. "하향식", "기억"또는 기타 원하는 것을 호출 할 수 있습니다. 이 접근 방식은 매우 직관적이며 구현하기가 매우 쉽습니다. 재귀 솔루션을 먼저 작성하고 소규모 테스트에서 테스트하고 메모 (이미 계산 된 값의 캐싱)를 추가하고 --- bingo! --- 끝났습니다.
일반적으로 재귀없이 상향식으로 작동하는 동등한 반복 프로그램을 작성할 수도 있습니다. 이 경우 이것은보다 자연스러운 접근 방법입니다. 1에서 50까지 반복하여 피보나치 수를 계산하십시오.
fib[0] = 0
fib[1] = 1
for i in range(48):
fib[i+2] = fib[i] + fib[i+1]
흥미로운 시나리오에서 상향식 솔루션은 일반적으로 이해하기가 더 어렵습니다. 그러나 일단 이해하면 일반적으로 알고리즘 작동 방식을 훨씬 명확하게 알 수 있습니다. 실제로, 사소한 문제를 해결할 때는 먼저 하향식 접근 방식을 작성하고 작은 예제에서 테스트하는 것이 좋습니다. 그런 다음 상향식 솔루션을 작성하고 두 솔루션을 비교하여 동일한 결과를 얻도록하십시오. 이상적으로 두 솔루션을 자동으로 비교하십시오. 이상적으로 많은 테스트를 생성하는 작은 루틴을 작성 하십시오.특정 크기까지 작은 테스트 --- 두 솔루션이 동일한 결과를 제공하는지 확인합니다. 그런 다음 프로덕션 환경에서 상향식 솔루션을 사용하지만 맨 아래 코드를 유지하십시오. 이렇게하면 다른 개발자가 자신이하는 일을 더 쉽게 이해할 수 있습니다. 상향식 코드는 이해하기 어렵고 심지어 작성했거나 자신이하는 일을 정확히 아는 경우에도 이해할 수 없습니다.
많은 애플리케이션에서 상향식 접근 방식은 재귀 호출의 오버 헤드로 인해 약간 더 빠릅니다. 스택 오버플로는 특정 문제에서 문제가 될 수 있으며 이는 입력 데이터에 따라 크게 달라질 수 있습니다. 동적 프로그래밍을 충분히 이해하지 못하면 스택 오버플로를 유발하는 테스트를 작성하지 못할 수도 있지만 언젠가는 여전히 이런 일이 발생할 수 있습니다.
문제 공간이 너무 커서 모든 하위 문제를 해결할 수 없기 때문에 하향식 접근 방식이 유일하게 실현 가능한 솔루션 인 문제가 있습니다. 그러나 "캐싱"은 입력에 해결해야 할 하위 문제의 일부만 필요하기 때문에 여전히 합리적인 시간에 작동합니다. 그러나 명시 적으로 정의하고 해결해야하는 하위 문제를 정의하기에는 너무 까다로워서 솔루션. 반면에 모든 하위 문제 를 해결해야 할 상황이 있습니다 . 이 경우 계속해서 상향식을 사용하십시오.
필자는 개인적으로 Word 랩 최적화 문제 로 단락 최적화를 위해 맨 아래를 사용하고 싶습니다 (Knuth-Plass 줄 바꿈 알고리즘을 찾으십시오. 적어도 TeX는 그것을 사용하고 Adobe Systems의 일부 소프트웨어는 비슷한 접근 방식을 사용합니다). 고속 푸리에 변환에 상향식을 사용 합니다.
피보나치 시리즈를 예로 들어 보겠습니다.
1,1,2,3,5,8,13,21....
first number: 1
Second number: 1
Third Number: 2
그것을 넣는 또 다른 방법은
Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21
처음 다섯 피보나치 수의 경우
Bottom(first) number :1
Top (fifth) number: 5
이제 재귀 피보나치 시리즈 알고리즘을 예로 들어 보겠습니다.
public int rcursive(int n) {
if ((n == 1) || (n == 2)) {
return 1;
} else {
return rcursive(n - 1) + rcursive(n - 2);
}
}
다음 명령으로이 프로그램을 실행하면
rcursive(5);
알고리즘을 자세히 살펴보면 다섯 번째 숫자를 생성하기 위해서는 세 번째와 네 번째 숫자가 필요합니다. 따라서 내 재귀는 실제로 top (5)에서 시작하여 아래쪽 / 낮은 숫자로갑니다. 이 접근법은 실제로 하향식 접근법입니다.
동일한 계산을 여러 번 수행하지 않도록 동적 프로그래밍 기술을 사용합니다. 이전에 계산 된 값을 저장하고 재사용합니다. 이 기술을 메모라고합니다. 현재 문제를 논의하는 데 필요하지 않은 메모 작성 이외의 동적 프로그래밍에는 더 많은 것이 있습니다.
위에서 아래로
원래 알고리즘을 다시 작성하고 메모 된 기술을 추가 할 수 있습니다.
public int memoized(int n, int[] memo) {
if (n <= 2) {
return 1;
} else if (memo[n] != -1) {
return memo[n];
} else {
memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
}
return memo[n];
}
그리고 우리는이 방법을 다음과 같이 실행합니다
int n = 5;
int[] memo = new int[n + 1];
Arrays.fill(memo, -1);
memoized(n, memo);
이 솔루션은 알고리즘이 최상위 값에서 시작하여 각 단계의 맨 아래로 이동하여 최상위 값을 얻음에 따라 여전히 하향식입니다.
상향식
그러나 문제는 첫 번째 피보나치 수에서와 같이 바닥에서 시작하여 위로 올라갈 수 있다는 것입니다. 이 기술을 사용하여 다시 작성하겠습니다.
public int dp(int n) {
int[] output = new int[n + 1];
output[1] = 1;
output[2] = 1;
for (int i = 3; i <= n; i++) {
output[i] = output[i - 1] + output[i - 2];
}
return output[n];
}
이제이 알고리즘을 살펴보면 실제로 낮은 값에서 시작하여 맨 위로 이동합니다. 5 번째 피보나치 수가 필요한 경우 실제로 1 번째를 계산하고 두 번째로 5 번째 숫자까지 세 번째로 계산합니다. 이 기술을 실제로 상향식 기술이라고합니다.
마지막 두 알고리즘은 동적 프로그래밍 요구 사항을 완전히 채 웁니다. 그러나 하나는 하향식이고 다른 하나는 상향식입니다. 두 알고리즘 모두 비슷한 공간 및 시간 복잡성을 가지고 있습니다.
다이나믹 프로그래밍은 종종 Memoization이라고합니다!
1. 메모 화는 하향식 기술 (주어진 문제를 분해하여 시작)이고 동적 프로그래밍은 상향식 기술 (사소한 하위 문제에서 주어진 문제를 향한 시작)
2. DP는 기본 사례에서 시작하여 솔루션을 찾고 위로 진행합니다. DP는 모든 하위 문제를 해결합니다.
필요한 하위 문제 만 해결하는 Memoization과 달리
DP는 지수 시간 무차별 대입 솔루션을 다항식 시간 알고리즘으로 변환 할 가능성이 있습니다.
DP는 반복적이므로 훨씬 더 효율적일 수 있습니다.
반대로, 메모 화는 재귀로 인한 오버 헤드에 대한 비용을 지불해야합니다.
더 간단하게, Memoization은 하향식 접근 방식을 사용하여 문제를 해결합니다. 즉 핵심 (주요) 문제로 시작한 다음 하위 문제로 나누고 이러한 하위 문제를 유사하게 해결합니다. 이 방법에서는 동일한 하위 문제가 여러 번 발생하고 더 많은 CPU주기를 소비 할 수 있으므로 시간 복잡성이 증가합니다. 동적 프로그래밍에서 동일한 하위 문제는 여러 번 해결되지 않지만 이전 결과는 솔루션을 최적화하는 데 사용됩니다.
간단히 하향식 접근 방식은 하위 문제를 반복해서 호출하기 위해 재귀를 사용합니다
. 상향식 접근 방식은 단일 호출 방식을 사용하지 않고 단일을 사용하므로 더 효율적입니다.
아래는 하향식 거리 편집 문제에 대한 DP 기반 솔루션입니다. 동적 프로그래밍의 세계를 이해하는 데 도움이되기를 바랍니다.
public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
int m = word2.length();
int n = word1.length();
if(m == 0) // Cannot miss the corner cases !
return n;
if(n == 0)
return m;
int[][] DP = new int[n + 1][m + 1];
for(int j =1 ; j <= m; j++) {
DP[0][j] = j;
}
for(int i =1 ; i <= n; i++) {
DP[i][0] = i;
}
for(int i =1 ; i <= n; i++) {
for(int j =1 ; j <= m; j++) {
if(word1.charAt(i - 1) == word2.charAt(j - 1))
DP[i][j] = DP[i-1][j-1];
else
DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
}
}
return DP[n][m];
}
집에서 재귀 구현을 생각할 수 있습니다. 이전에 이와 같은 문제를 해결하지 않았다면 상당히 좋고 도전적입니다.
하향식 : 지금까지 계산 된 값을 추적하고 기본 조건이 충족되면 결과를 반환합니다.
int n = 5;
fibTopDown(1, 1, 2, n);
private int fibTopDown(int i, int j, int count, int n) {
if (count > n) return 1;
if (count == n) return i + j;
return fibTopDown(j, i + j, count + 1, n);
}
상향식 : 현재 결과는 하위 문제의 결과에 따라 다릅니다.
int n = 5;
fibBottomUp(n);
private int fibBottomUp(int n) {
if (n <= 1) return 1;
return fibBottomUp(n - 1) + fibBottomUp(n - 2);
}