오늘의 학습 키워드

다이나믹 프로그래밍 (=동적프로그래밍)

오늘 공부한 내용

TopDown방식, UpBottom방식, 점화식, range() 함수

오늘의 회고

오늘은 재귀함수를 이용해서 풀이를 할려고 했지만 잘 풀리지 않았다.

이것은 아닌 것 같아 반복문을 활용해보기 시작했다. 하지만 반복문에 재귀함수를 돌릴려고 하다보니 좀처럼 잘 되지 않았다.

3시간정도가 지났을까 아무리 생각을 해봐도 비슷한 솔루션만 도출되었다.

도저히 알 길이 없어 다이나믹 프로그래밍을 유튜브에 검색을 해보았다.

유튜브에 검색을 해보니 여러 영상들이 보였다. 그 중 하나를 골라 다이나믹 프로그래밍의 원리를 조금이나마 공부를 하게 되었다.

다이나믹 프로그램이 가능한 조건

  1. 큰 문제를 작은 문제로 분할 가능해야 한다.
  2. 작은 문제에서 구한 정답이 그것을 포함하는 큰 문제에서도 동일해야 한다.

이것이 무슨 소리일까?

예를 들어 유명한 피보나치를 보자 피보나치는 아래와 같이 생겼다.

n = 6일때, 1, 1, 2, 3, 5, 8

피보나치는 첫번째 숫자, 두번째 숫자는 초기화를 시킨다. 첫번째 숫자와 두번째 숫자를 더해 다음 숫자를 만든다.

마찬가지로 두번째 숫자와 첫번째 숫자를 더해 다음 숫자를 만든다.

즉, 큰 문제 8을 만들기 위해 3과 5를 더했고, 5는 2와 3을 더했고, 3은 1와 2을 더하는 등 작은 문제로 분할이 가능하다!

또한 다음 수 n = 7이 되었을 때를 가정하면 5와 8을 더한 13이 되기때문에 2번의 조건도 충족하는 것을 알 수가 있다.

다이나믹 프로그램은 용어가 많았다.

첫번째로 TopDown방식, BottomUp방식 두가지로 나뉜다고 한다.

TopDown 방식

TopDown 방식은 트리 구조를 생각할 때 Top에서 밑으로 내려가면서 큰 결과가 만들어지는 구조이다.

재귀함수를 사용하여 구현한다.

피보나치를 예로 들어보자

def solution(n):
	if n == 1 or n == 2:
		return 1
	return solution(n - 1) + solution(n - 2)

BottomUp 방식

BottomUp 방식도 트리 구조를 생각할 때 Bottom에서 위로 올라가며 큰 결과가 만들어지는 구조이다.

반복문을 통해 구현한다.

피보나치를 예로 들어보자

n = 5 # 시행 횟수
memo = [] * n
memo[0] = 1
memo[1] = 1

for i in range(2, n):
	memo[i] = memo[i - 1] + memo[i - 2]

하지만 TopDown의 방식과 같이 구현하게 된다면 문제가 발생한다. 아래 트리구조에서 보이는 것처럼 8을 구현하고 싶다면 다음과 같은 문제가 발생한다.

트리구조

바로 같은 동일 함수를 여러번 호출하는 것이다. 예들들어 위의 f(3), f(2) 는 중복된다.

그래서 나오는 개념이 메모라이제이션이다.

메모이제이션

이전 계산한 값을 메모리에 저장하여 재사용성을 높이는 방식이다. 다음과 같이 피보나치를 예로 들어보자

n = 5 # 시행횟수
memo = [1] * n

def solution(n):
    global memo

    if n == 1 or n == 2:
        return 1

    if memo[n - 1] != 1:
        return memo[n - 1]

    memo[n - 1] = solution(n - 1) + solution(n - 2)
    return memo[n - 1]

점화식

패턴을 일반화(수학적으로) 시킨 방식

f(n) = f(n - 1) + f(n - 2)

오늘은 많은 것을 배운것 같지만 나중에 또 생각이 날지는 모르겠다.

또한 이런 모든 것들을 배웠는데도 불구하고 BottomUp 방식으로 구현해야한다는 것은 알았지만 패턴을 일반화 시키지 못해 결국 풀이해법을 보고 말았다 ㅠㅠ

다음에는 조금 더 패턴을 찾아보고 적용시킬 수 있도록 노력해야겠다.

결과물

문제내용

숫자만큼 파스칼 삼각형을 만들어 리턴해야한다.

파스칼 삼각형은 아래와 같다.

PascalTriangleAnimated2

풀이방법

n = 5일때, 아래와 같은 패턴을 띈다.

  1. 리스트를 [1]*n 만큼 초기화 해놓는다. (=리스트 컴프리헨션)
lists = [
    [1],                # index = 0
    [1, 1],             # index = 1
    [1, 1, 1],          # index = 2 
    [1, 1, 1, 1]        # index = 3
    [1, 1, 1, 1, 1]     # index = 4
]
  1. 2번째 이전은 더하는 과정이 없으므로 생략한다. 이미 1번에서 만들어졌음
  2. 리스트는 전부 1로 초기화 되어있기 때문에 아래와 같은 패턴이 나타난다.
lists[2][1] = lists[1][0] + lists[1][1]
# ---------------------------------------
lists[3][1] = lists[2][0] + lists[2][1]
lists[3][2] = lists[2][1] + lists[2][2]
# ---------------------------------------
lists[4][1] = lists[3][0] + lists[3][1]
lists[4][2] = lists[3][1] + lists[3][2]
lists[4][3] = lists[3][2] + lists[3][3]

즉, 첫번째 인덱스는 2부터 시작하여 4가 마지막이 되고 두번째 인덱스는 1부터 점차증가한다.

range()는 0부터 n-1까지 돈다.

from typing import List

class Solution:
    def generate(self, numRows: int) -> List[List[int]]:
        dp = [[1] * i for i in range(1, numRows+1)]
		
        for i in range(2, numRows):
            for j in range(1, i):
                dp[i][j] = dp[i-1][j-1] + dp[i-1][j]

        return dp

s = Solution()
print(s.generate(5))

댓글남기기