데이터 구조-동적 프로그래밍 알고리즘

1. 동적 프로그래밍 정의 및 기본 아이디어

동적 프로그래밍은 복잡한 문제를 간단한 하위 문제로 분해하는 해결 방법입니다.

동적 프로그래밍의 기본 아이디어: 일반적으로 특정 최적 속성이 있는 문제를 해결하는 데 사용되며 각 문제를 한 번만 해결하려고 합니다. 주어진 하위 문제에 대한 솔루션이 계산되면 동일한 하위 문제가 해결되도록 기억되고 저장됩니다. -다음번에 문제가 필요하니 문제를 풀 때는 표를 직접 찾아보세요. 동적 프로그래밍은 반복적인 계산을 피하면서 상향식(재귀적보다는 반복적으로)으로 문제를 해결합니다.

재귀의 경우 이러한 하위 문제를 해결한 다음 하위 문제에 대한 솔루션을 결합하여 원래 문제에 대한 솔루션을 얻습니다. 차이점은 이러한 하위 문제가 중복되어 하위 문제가 해결된 후 다시 해결될 수 있다는 것입니다.

따라서 이러한 하위 문제에 대한 답을 저장할 수 있으며 나중에 하위 문제가 사용되는지 여부에 관계없이 계산만 하면 결과가 테이블에 채워집니다. 다음에 사용할 때는 결과를 직접 사용하십시오. 이것이 동적 프로그래밍 방법의 기본 아이디어이다.

예:

# 动态规划实现斐波那契数列
def fib(n):
    # 递推公式的边界条件
    if n == 0 or n == 1:
        return n

    dp = [0, 1, 0]

    for i in range(1, n):

        sum = dp[0] + dp[1]
        dp[0] = dp[1]
        dp[1] = sum

    return dp[1]
print(fib(10))

# 时间复杂度:O(n)
# 空间复杂度:O(1)

# 递归思路
def fib(n):
    if n == 0 or n == 1:
        return n
    return fib(n-1) + fib(n-2)
print(fib(10))

# 时间复杂度:O(2^n)
# 空间复杂度:O(n)

2. 동적 프로그래밍의 기본 단계

1. 재귀 또는 재귀 수식의 결과와 첨자의 의미를 저장할 수 있는 배열을 결정합니다. 일반적으로 1차원 배열(더 간단한 질문)이나 2차원 배열(약간 더 어려움)을 사용하여 저장하는데 여기서는 항상 dp[]로 표현되는데 그중 1차원 dp에 대한 이해가 필요합니다. [i] (인덱스 위치에 해당하는 배열. 요소), 2차원 배열 dp[i][j] (2차원 배열에서는 일반 용어로 행의 해당 인덱스 위치에 있는 요소입니다. 및 열);

2. dp 배열 초기화. 1차원 배열과 2차원 배열의 경우 초기 값은 다음에 결과가 겹쳐 보일 때 도움이 될 수 있습니다.

예: 피보나치 dp[0] = 0, dp[1] = 1, dp[2] = dp[0] + dp[1].

3. 상태 전이 방정식(재귀 공식)을 결정합니다. 상태 전이란 이전 단계의 상태를 기반으로 이번 단계의 상태(즉, 서로 다른 단계 간의 연결)를 도출하는 것으로, 실제로 이전에 사용된 결과가 이 단계에 적용 가능한지 여부를 알아보는 것이다.

예를 들어, 피보나치의 fib(4)는 동일한 계산 방법 fib(4) = fib(3) + fib(2)를 사용하여 두 번 재귀적으로 계산하므로 이 계산 방법은 재귀 공식입니다. 동적 정규화에서는 첫 번째 계산 결과를 저장하고 다음에 그 결과를 직접 사용합니다.

4. 순회 순서를 결정합니다. 예: 피보나치 순회는 앞에서 뒤로 진행됩니다. 순회 순서를 결정하면 결과를 더 빨리 얻을 수 있습니다.

5. dp 배열을 예로 들어 보겠습니다. 위의 단계를 완료한 후 추론할 몇 가지 예를 제시하거나 코드를 완성한 후 코드 편집기를 사용할 수 있습니다. 저는 코드 디버깅을 위해 vscode와 pycharm을 사용합니다.

여기서 LeetCode는 이러한 5단계를 설명합니다 .

1단계: 배열 dp를 결정하고 m+1개 행과 n+1개 열로 구성된 배열을 만들어 각 계산의 피연산자를 저장합니다. word1과 word2는 관련 연산을 수행하기 위해 존재하므로 2차원 배열을 사용하여 word1을 word2로 변환하고 사용되는 연산 횟수를 최소한으로 저장할 수 있습니다.

dp = [[0] * (n+1) for i in range(m+1)

여기서 아래 첨자 dp[i][j] 의 의미를 이해해야 합니다 . word1의 첫 번째 i 문자(아래 첨자 i-1)와 word2의 첫 j 문자(아래 첨자 j-1) 사이의 최소 편집 거리, 예: word1 = 'op', word2 = 'app', dp[2][2]=X. 이는 'op'와 'ap' 사이의 최소 편집 거리, 즉 변환에 필요한 최소 양을 나타냅니다. 'op'에서 'ap'으로 작동합니다.

2단계: 배열 dp의 초기 값을 결정합니다. 테이블에서 초기화를 구현하려면 dp[i-1][j-1], 즉 dp[i][j] 간의 관계를 이해해야 합니다.

예: word1 및 word2에 해당하는 첫 번째 문자 dp[i][j]를 찾고 연산하려면 이전 단계의 최소 피연산자 dp[i-1][j-1을 찾아야 합니다. ].그러나 해당 요소는 실제로 비어 있으므로 초기값을 결정할 때 dp[i-1][j-1] = 0인 상황을 고려해야 합니다.

따라서 이제 초기값의 첫 번째 행 요소는 덧셈 연산 후 word2가 되는 빈 문자의 피연산자이고, 첫 번째 열의 요소는 word1이 삭제 연산 후 빈 문자열이 되는 피연산자임을 알 수 있습니다. .

행: 단어1

열: word2

 “ ”

없는

AP 응용 사과

“ ”

없는

0 1 2 4 5
영형 1
작전 2
반대
오빠 4
dp[0][0] = 0        # 第一个元素赋值为0
    for i in range(m+1):     # 对word1里的元素全部做删除操作
        dp[i][0] = i
    for j in range(n+1):     # 对word2里的元素进行添加操作
        dp[0][j] = j

세 번째 단계는 재귀 공식을 결정하는 것입니다. 이 질문에서 가장 어려운 부분은 재귀 공식입니다. 재귀 공식을 이해하기 전에 먼저 가능한 상황을 결정합니다.

1. 변환 중 word1[i-1] == word2[j-1]이면 연산을 수행할 필요가 없으며 첨자 dp[i-1][j-1]의 결과를 직접 유지할 수 있습니다. 이때까지 즉, dp[i][j] == dp[i-1][j-1]입니다.

2. word1[i-1] != word2[j-1]인 경우 연산이 발생하는 세 가지 방식을 고려해야 합니다.

즉, word1은 word1[i-1]을 대체합니다. dp[i][j] = dp[i-1][j-1] + 1    

       word2는 요소를 삭제합니다: dp[i][j] = dp[i][j-1] + 1

       Word1은 요소를 삭제합니다: dp[i][j] = dp[i-1][j] + 1. 이는 word2가 요소를 추가하는 것과 동일합니다.

dp[i-1][j-1] dp[i-1][j]
dp[i][j-1] dp[i][j]

여기서 + 1이 필요한 이유는 무엇입니까?

이는 이 세 가지 작업에서 마지막 결과가 dp 배열에 유지되어 dp[i][j]의 결과를 풀어야 하기 때문입니다. 마지막으로 피연산자의 최소값(최소 피연산자 개수)을 구하여 +1을 구한다.

if word1[i-1] == word2[j-1]:
    dp[i][j] = dp[i-1][j-1]
else:
    dp[i][j] = min(dp[i-1][j-1],dp[i][j-1],dp[i-1][j])+1

네 번째 단계는 배열의 순회 순서를 결정하는 것입니다. 당연히 4개의 재귀 공식을 결합해야 하며 순회 순서는 왼쪽에서 오른쪽, 위에서 아래로임을 알 수 있습니다.

5단계: 예시를 통해 검증하고 추론하기

여기서는 파생을 위해 word1='oppa' 및 'apple'을 사용합니다.

행: 단어1

열: word2

 “ ”

없는

AP 응용 사과

“ ”

없는

0 1 2 4 5
영형 1 1 2 4 5
작전 2 2 1 2 4
반대 2 1 2
오빠 4 2 2

대부분의 동적 프로그래밍 알고리즘 질문은 이 5단계를 기반으로 한다고 볼 수 있으므로 5단계의 의미에 주의를 기울여야 합니다.

동적 프로그래밍의 개념을 더 잘 이해하려면 여전히 더 많은 질문을 공부해야 합니다.

파이썬 코드 구현:

def minDistance(word1, word2):

   m = len(word1)
   n = len(word2)
   # 当word1为空的时候,word2的长度就是需要编辑的距离
   if m == 0:
       return n
   
   # 当word2为空的时候,word1的长度就是需要编辑的距离
   if n == 0:
       return m
        
   # 创建一个m+1行n+1列的数组
   dp = [[0]*(n+1) for i in range(m+1)]

   dp[0][0] = 0
   
   # 从 word1[:i] 变成 空串(word2[:0]), 操作即**删除** i 次直到将 word1[:i] 删光到 空串
   for i in range(1,m+1):
       dp[i][0] = i
   
# 从 空串(word1[:0]) 变成 word2[:j], 操作即**插入** j 次直到将 空串 变成 word2[:j]
   for j in range(1,n+1):
       dp[0][j] = j

   for i in range(1, m+1):
       for j in range(1, n+1):
            # 考虑字符是否相等
            if word1[i-1] == word2[j-1]:
               # 如果相等就是上一步操作的步数  
               dp[i][j] = dp[i-1][j-1]
            else:
               # 如果不相等就是附近三个最小值+1
               dp[i][j] = 1+min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) 

    return dp[m][n]

s1 = "oppa"
s2 = "apple"
print(minDistance(s1,s2))

여기까지 찾아주신 모든 분들께 감사드리며, 동시에 동적 계획을 이해할 수 있도록 정보를 제공해주신 뛰어난 전문가 여러분께 감사드립니다.

동일한 글이 있을 경우 작성자에게 알려주시고 링크를 첨부해 주시면 본 글 내 해당 위치에 대한 링크를 첨부해 드리겠습니다. 감사합니다.

추천

출처blog.csdn.net/m0_71145378/article/details/126837568