기술나눔

알고리즘 연구 노트(8) - 동적 프로그래밍의 기초

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

목차

기본 내용:

동적 프로그래밍:

동적 프로그래밍을 이해하는 데 문제가 발생합니다.

분석: (폭력적인 역추적)

코드 예:

잔인한 검색:

Dfs 코드 예: (검색)

무차별 재귀로 생성된 재귀 트리:

메모된 검색:

코드 예:

동적 프로그래밍:

코드 예: (동적 프로그래밍, 가장 작은 하위 문제부터 시작)

실행 프로세스(동적 프로그래밍):

분석: (동적 프로그래밍)

공간 최적화:

코드 예:

분석:


기본 내용:

동적 프로그래밍이란 무엇인지, 동적 프로그래밍이 수단으로 어떤 문제를 해결할 수 있는지, 동적 프로그래밍의 분류, 특정 분류로 해결할 수 있는 특정 문제의 분류 등을 다룬다.

동적 프로그래밍:

문제를 일련의 작은 하위 문제로 분해하고 하위 문제 솔루션을 저장하여 반복 계산을 방지하여 시간 효율성을 크게 향상시키는 중요한 알고리즘 패러다임입니다.

동적 프로그래밍을 이해하는 데 문제가 발생합니다.

이 문제는 계단을 오르는 경우를 통해 소개됩니다. 계단이 총 n개라면 각 계단마다 1~2계단씩 올라갈 수 있습니다.

분석: (폭력적인 역추적)

이 질문의 목표는 솔루션의 수를 찾는 것입니다.역추적을 통해 모든 가능성을 철저히 소진하는 것을 고려할 수 있습니다. . 구체적으로 계단 오르기를 여러 단계의 선택 과정으로 상상해 보세요. 바닥에서 시작하여 각 단계마다 한두 단계를 선택하고, 계단 꼭대기에 도달할 때마다 옵션 수에 1을 추가하고, 올라갈 때 옵션 수를 늘립니다. 당신은 계단 꼭대기에 도달했습니다.

코드 예

  1. # python代码示例
  2. def backrack(choices,state,n,res) :
  3. if state == n :
  4. res[0] += 1
  5. for choice in choices :
  6. if state + choice > n :
  7. continue
  8. backrack(choices,state+choice,n,res)
  9. def climbing_stairs_backrack(n) :
  10. choices = [1,2]
  11. state = 0
  12. res = [0]
  13. backrack(choices,state,n,res)
  14. return res[0]
  15. n = int(input())
  16. print(climbing_stairs_backrack(n))
  1. // c++代码示例
  2. void backrack(vector<int> &choices, int state, int n, vector<int> &res)
  3. {
  4. if (state == n )
  5. {
  6. res[0]++ ;
  7. }
  8. for (auto &choice : choices)
  9. {
  10. if (state + choice > n)
  11. {
  12. continue ;
  13. }
  14. backrack(choices, state + choice, n, res)
  15. }
  16. }
  17. int climbingStairsBackrack(int n)
  18. {
  19. vector<int> choices = {1 , 2 } ;
  20. int state = 0 ;
  21. vector<int> res = [0] ;
  22. backrack(choices, state, n, res) ;
  23. return res[0] ;
  24. }

잔인한 검색:

역추적 알고리즘은 일반적으로 문제를 명시적으로 해체하지 않지만 문제를 일련의 의사 결정 단계로 처리하고 휴리스틱과 가지치기를 통해 가능한 모든 솔루션을 검색합니다.

우리는 문제 분해의 관점에서 이 질문을 분석해 볼 수 있습니다. i번째 레벨로 올라가기 위한 dp[i] 해가 있다고 가정하면 dp[i]가 원래 문제이고 하위 문제는 다음과 같습니다.

dp[i-1],dp[i-2],dp[1],dp[2]

매 라운드마다 1, 2계단만 올라갈 수 있기 때문에 i번째 계단에 섰을 때 이전 라운드에서는 i-1, i-2계단에만 설 수 있었습니다. 즉, i-1 또는 i-2 수준에서 i-수준으로만 이동할 수 있습니다.

이로부터 우리는 중요한 추론을 할 수 있습니다. i-1 수준에 오른 계획 수와 i-2 수준에 오른 계획 수를 더한 것은 i 수준에 오른 계획 수와 같습니다. -레벨. 공식은 다음과 같습니다.

dp[i] = dp[i-1] + dp[i-2]

이는 건물 오르기 문제에는 재귀적 관계가 존재하며, 하위 문제의 해법을 구성함으로써 원래의 문제를 해결할 수 있음을 의미한다.

Dfs 코드 예: (검색)

  1. # python 代码示例
  2. def dfs(i : int) -> int :
  3. if i == 1 or i == 2 :
  4. return i
  5. count = dfs(i - 1) + dfs(i - 2)
  6. return count
  7. def climbing_stairs_dfs(n : int) -> int :
  8. retunr dfs(n)
  1. // c++ 代码示例
  2. int dfs(int i)
  3. {
  4. if (i == 1 || i == 2)
  5. {
  6. return i ;
  7. }
  8. int count = dfs(i - 1) + dfs(i - 2);
  9. return count ;
  10. }
  11. int climbingStairsDFS(int n)
  12. {
  13. retunr dfs(n) ;
  14. }

폭력적인 재귀로 생성된 재귀 트리

위에서 언급한 재귀 트리의 중복 문제를 해결하기 위해 메모리 탐색 방법을 사용하여 반복적으로 구성된 다수의 동일한 하위 트리를 제거함으로써 계산 효율성을 높일 수 있습니다. (중복되는 하위 문제

메모된 검색:

겹치는 모든 하위 문제를 한 번만 계산하려면 nem 배열을 선언하여 각 ​​하위 문제의 해를 기록하고 검색 프로세스 중에 겹치는 하위 문제를 정리해야 합니다.

  1. dp[i]가 처음 계산되면 이후 사용을 위해 nem[i]에 기록됩니다.
  2. dp[i]를 다시 계산할 때 하위 문제의 반복 계산을 피하기 위해 nem[i]에서 직접 결과를 얻습니다.

코드 예:

  1. # python 代码示例
  2. def dfs(i : int, mem : list[int]) -> int :
  3. if i == 1 or i == 2 :
  4. return i
  5. if mem[i] != -1 :
  6. return mem[i]
  7. count = dfs(i - 1, mem) + dfs(i - 2, mem)
  8. # 记录dfs(i)
  9. mem[i] = count
  10. return count
  11. def climbing_stairs_dfs_mem(n : int) -> int :
  12. mem = [-1] * (n + 1)
  13. return dfs(n, mem)
  1. // c++ 代码示例
  2. int dfs(int i, vector<int> &mem)
  3. {
  4. if (i == 1 || i == 2)
  5. {
  6. return i ;
  7. }
  8. if (mem != -1)
  9. {
  10. return mem[i] ;
  11. }
  12. int count = dfs(i - 1, mem) + dfs(i - 2, mem) ;
  13. mem[i] = count ;
  14. return count ;
  15. }
  16. int climbingStairsDFSMem(int n)
  17. {
  18. vector<int> mem(n + 1, -1) ;
  19. return dfs(n, mem) ;
  20. }

메모한 후에는 겹치는 모든 하위 문제를 한 번만 계산하고 시간 복잡도는 O(n)으로 최적화됩니다.

동적 프로그래밍:

메모화된 검색은 원래 문제(루트 노드)에서 시작하여 가장 작은 알려진 하위 문제(리프 노드)를 해결할 때까지 더 큰 하위 문제를 더 작은 하위 문제로 재귀적으로 분해합니다. . 이후, 역추적을 통해 하위 문제에 대한 솔루션을 계층별로 수집하여 원래 문제에 대한 솔루션을 구성합니다.

대조적으로, 동적 프로그래밍은 "하단에서 상단까지" 접근 방식입니다. 즉, 가장 작은 하위 문제에 대한 솔루션부터 시작하여 원래 문제에 대한 솔루션을 얻을 때까지 더 큰 하위 문제에 대한 솔루션을 반복적으로 구축합니다.

동적 프로그래밍에는 역추적 프로세스가 포함되어 있지 않으므로 재귀를 사용하지 않고 루프 반복을 사용하여 구현하면 됩니다.

코드 예: (동적 프로그래밍, 가장 작은 하위 문제부터 시작)

  1. # python 代码示例
  2. def clibing_stairs_dp(n) :
  3. if n == 1 or n == 2 :
  4. return n
  5. dp = [0] * (n + 1)
  6. dp[1], dp[2] = 1, 2
  7. for i in range(3,n + 1) :
  8. dp[i] = dp[i-1] + dp[i- 2]
  9. return dp[n]
  1. // c++ 代码示例
  2. int climbingStairsDP(int n)
  3. {
  4. if (n == 1 || n == 2)
  5. {
  6. retunr n ;
  7. }
  8. vector<int> dp(n + 1, -1) ;
  9. dp[1] = 1 ;
  10. dp[2] = 2 ;
  11. for (int i = 3 ; i <= n ; i++)
  12. {
  13. dp[i] = dp[i - 1] + dp[i- 2] ;
  14. }
  15. return dp[n] ;
  16. }

실행 프로세스(동적 프로그래밍):

분석: (동적 프로그래밍)

역추적 알고리즘과 마찬가지로 동적 프로그래밍도 "상태" 개념을 사용하여 문제 해결의 특정 단계를 나타냅니다. 각 상태는 하위 문제 및 해당 로컬 최적 솔루션에 해당합니다. 예: 계단 오르기 문제의 상태는 현재 계단의 차수 i로 정의됩니다.

위의 내용을 바탕으로 동적 용어에 대한 일반적인 용어를 요약할 수 있습니다.

  1. dp 배열은 {dp table}이라고 하며, dp[i]는 상태 i에 해당하는 하위 문제에 대한 솔루션을 나타냅니다.
  2. 최소 하위 문제(첫 번째 및 두 번째 계단)에 해당하는 상태를 초기 상태라고 합니다.
  3. 재귀 공식 dp[i] = dp[i-1] + dp[i-2]를 상태 방정식이라고 합니다.

공간 최적화:

dp[i]는 dp[i-1] 및 dp[i-2]에만 관련됩니다.

모든 하위 문제에 대한 솔루션을 저장하기 위해 배열을 사용하는 대신 앞으로 스크롤하려면 두 개의 변수만 필요합니다.

코드 예:

  1. # python 代码示例
  2. def clibing_stairs_dp_comp(n) :
  3. if n == 1 or n == 2 :
  4. return n
  5. a, b = 1, 2
  6. for _ in range(3, n + 1) :
  7. a, b = b , a + b
  8. return b
  1. // c++ 代码示例
  2. int climbingStairsComp(int n)
  3. {
  4. if (n == 1 || n == 2)
  5. {
  6. return n ;
  7. }
  8. int a = 1 , b = 2 ;
  9. for (int i = 3 ; i <= n ; i++)
  10. {
  11. int temp = b ;
  12. b = a + b ;
  13. a = temp ;
  14. }
  15. return b ;
  16. }

분석:

배열 dp가 차지하는 공간은 생략되고, 공간 복잡도는 O(n)에서 O(1)로 감소됩니다.

동적 프로그래밍 문제에서 현재 상태는 제한된 수의 이전 상태에만 관련되어 있으며, 이때 "차원 축소"를 통해 필요한 상태만 유지하고 메모리 공간을 절약할 수 있습니다. . 이 공간 최적화 기술을 "롤링 변수" 또는 "롤링 배열"이라고 합니다.