반응형
snowman95
코딩수련장
snowman95
전체 방문자
오늘
어제
  • 분류 전체보기 (229)
    • 앱테크 (3)
    • 옵시디언 (5)
    • 드라마, 영화 (1)
    • 개발자 이야기 (23)
    • 프로젝트 (10)
      • 프로젝트 방법론 (7)
      • 프로젝트 기록 (2)
      • Github (1)
    • 개발 지식 (0)
      • 디자인 패턴 (0)
    • 프론트엔드 개발 (5)
      • 테크트리 (2)
      • React.js (19)
      • ReactNative (2)
      • Next.js (6)
      • GraphQL (6)
      • 패키지 매니저 (2)
      • 라이브러리 (3)
      • 상태관리 라이브러리 (4)
      • Web 지식 (3)
      • HTML CSS (26)
      • Javascript (16)
      • 도구 (Tool) (3)
      • 성능 최적화 (1)
      • 디자인시스템 (0)
    • Python (53)
      • 모음집 (1)
      • 문법 (12)
      • 라이브러리 (15)
      • 알고리즘 (10)
      • 백준 문제풀이 (9)
      • 코딩테스트 (2)
      • 도구 (Tool) (3)
    • C++ (20)
      • 알고리즘 (6)
      • 삼성SW기출 (6)
      • 삼성 A형 (6)
    • 데이터사이언스 (1)
    • 인프라 (9)
      • 하드웨어 지식 (4)
      • Ansible (2)
      • Database (2)
      • 쉘스크립트 (1)
    • 주식 (0)
    • 취업 준비 (4)
      • 취업 이야기 (0)

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • 전공 요약 #네트워크
  • 기계식키보드 #nuphy
  • 면접
  • C++
  • 전공요약
  • 전공 요약 #운영체제
  • Next.js #graphql #tailwind.css
  • A형
  • 삼성SW역량테스트
  • nextjs
  • 티스토리챌린지
  • 삼성SDS
  • 언어
  • 오블완
  • 백준
  • GraphQL
  • 나의 해방일지
  • 알고리즘
  • 전공 요약 #데이터베이스
  • 공간복잡도

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
snowman95

코딩수련장

파이썬(python) 알고리즘 - 동적 계획법 푸는 방법
Python/알고리즘

파이썬(python) 알고리즘 - 동적 계획법 푸는 방법

2021. 4. 28. 23:44
728x90
반응형

DP문제를 풀어나가는 순서

1. 문제의 조건대로 손으로 그려보며 반복되는 부분을 찾습니다.

 

2. dp테이블 dp[i]는 무엇을 의미하는지 정의해봅니다.  

dp[i] = ???

 

3. 디테일한 점화식을 세웁니다.

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

 

4. 필요하다면 dp 테이블을 확장합니다.

dp[i] → dp[i][j]

 

5. 중복되는 부분을 어떻게 활용할 수 있을지 생각합니다.

어떤 방법으로도 점화식 세워지지 않으면 계산 과정이나 정의의 순서를 뒤집어서 생각해봅니다.

예를 들면 계산 과정을 완전히 거꾸로 뒤집어보는 발상을 해봅니다.

즉 1+1=2 이 아니라 2=1+1 이라는 발상을 하는 것입니다.

1번으로 되돌아가서 다시 진행합니다.

 

DP문제 유형이라고 어떻게 판단하는가?

문제만 보면 완전 탐색과 그리디와 헷갈릴 수 있다.

완탐이라기엔 시간이 부족하고

그리디라기엔 항상 탐욕적으로 취해서는 최적해가 나오지 않는다.

 

 

문제 예시를 통해 알아보기

 

동적 계획법 1 단계

i번째 집을 각각의 색으로 칠할 때, 1~i번째 집을 모두 칠하는 최소 비용으로 부분문제를 정의해봅시다.

www.acmicpc.net

 

아래는 제가 문제 풀면서 생각했던 과정을 녹여보았습니다.

백준 1463 : 1로 만들기

 

1463번: 1로 만들기

첫째 줄에 1보다 크거나 같고, 106보다 작거나 같은 정수 N이 주어진다.

www.acmicpc.net

순서 내용
1. 손으로 그려보며 반복되는 부분 찾기 10 → 5 → 4 → 2 → 1
10 → 9 → 3 → 1
 9 → 3 → 1
 3 → 1
2. dp테이블 dp[i] 의미 정의 dp[i] = i번 연산했을때 최소값
3. dp테이블 채우는 시뮬레이션 해보며 디테일한 점화식을 세우기 n=10 일때
1번 연산했을때 10//2 → 5 혹은 10-1 → 9 
1번 연산했을때 최소값인 5를 택하는 것 보다 9를 택하는게 더 나음.
하지만 끝까지 가보면 9를 택하는 것이 더 나은 선택임.
10 → 5 → 4 → 2 → 1
10 → 9 → 3 → 1

처음 정의로는 점화식 세우기 불가능. 다른 정의가 필요함.
4. 점화식 세워지지 않으면 계산 과정이나 정의의 순서를 뒤집어봅니다.
정의에서 앞뒤 순서를 바꿔보니 위의 문제가 해결됨.

n에서 1로 내려가는 것이 아니라
1에서 부터 시작해서 n을 만드는 방법을 찾는 것임.

n=10은
1+1+1+1+1+1+1+1+1+1 = 10회 연산
1*2*2*2+1+1 = 5회 연산
1*3*3+1 = 3회 연산

기존 : dp[i] = i번 연산했을때 최소값
변경 : dp[i] = i라는 숫자를 만들 수 있는 최소 연산 횟수
5. 재정의된 dp테이블로 시뮬레이션 n=10 일때
1번 연산했을때 10 → 5 혹은 10 → 9 
1번 연산했을때 최소값인 5를 택하는 것 보다 9를 택하는게 더 나음.
10 → 5 → 4 → 2 → 1
10 → 9 → 3 → 1

 

최종 풀이

dp[i] = i라는 숫자를 만들 수 있는 최소 연산 횟수

1부터 n까지 차례로 올라가면서
*2, *3, +1을 하여 값을 만들어가며 해당 값을 만든 최소연산횟수 갱신

import sys
dp = [int(1e9)] * 3000003
n = int(sys.stdin.readline())
dp[1] = 0

for i in range(1, n+1):
    dp[i*2] = min(dp[i*2], dp[i]+1)
    dp[i*3] = min(dp[i*3], dp[i]+1)
    dp[i+1] = min(dp[i+1], dp[i]+1)
print(dp[n])

 

백준 10844 : 쉬운 계단 수

 

10844번: 쉬운 계단 수

첫째 줄에 정답을 1,000,000,000으로 나눈 나머지를 출력한다.

www.acmicpc.net

 

순서 내용
1. 손으로 그려보며 반복되는 부분 찾기 n=1 일때 1~9 : 9개
n=2 일때 
1 뒤에 0,2 가 올 수 있음 = 10, 12
1 앞에 2가 올 수 있음 = 21
2 뒤에 1,3 가 올 수 있음 = 21, 23
2 앞에 1,3 가 올 수 잇음 = 12, 32
...
9 뒤에 8만 올 수 있음 = 98
9 앞에 8만 올 수 있음 = 89
2. dp테이블 dp[i] 의미 정의 특정 수의 앞뒤로 숫자를 끼워넣어서 새로운 숫자를 만드려고 하니, 불필요한 중복이 생겨버림
3. 중복되는 부분을 어떻게 활용할 수 있을지 생각해봅니다.

우선 특정 수의 뒤에만 숫자를 끼워넣어서 새로운 숫자를 만들도록 변경

n=1 일때 1~9 : 9개
n=2 일때 
0 뒤에 1이 올 수 있음 = 01
1 뒤에 0,2 가 올 수 있음 = 10,12
2 뒤에 1,3 가 올 수 있음 = 21, 23
...
8 뒤에 7,9 가 올 수 있음 = 87, 89
9 뒤에 8만 올 수 있음 = 98

n=3 일때
10 뒤에 1 올 수 있음  = 101
12 뒤에 1,3 올 수 있음 = 121, 123

10에서 0 뒤에 1이 온다는것을 이미 알고 있었음.
12에서 2뒤에 1이나 3이 와서 2개의 숫자를 만들 수 있다는 것도 암.

이것을 이용하여 dp테이블 정의가능.

dp[i][j] = i라는 숫자로 시작하는 길이 j의 숫자 개수
4. 재정의된 dp테이블로 시뮬레이션 dp[0][2] = 01 (1개)
dp[1][2] = 10, 12 (2개)
dp[2][2] = 21, 23 (2개)
...
dp[8][2] = 87, 89 (2개)
dp[9][2] = 98 (1개)

dp[0][3] = 010, 012 (2개) = 0 + dp[1][2]
dp[1][3] = 121, 123 = 1 + dp[2][2]
           = 101 = 1 + dp[0][2]
...
dp[9][3] = 987, 989 = 9 + dp[8][2]

시뮬레이션 해보면 3가지 경우가 나오는 것 확인할 수 있다.
1) 0으로 시작할때
2) 9로 시작할때
3) 그외의 숫자로 시작할때
5. 점화식 디테일하게 세우기 for length in range(2,n+1):
    for num in range(0,10):
        if num == 0:
            dp[num][length] = dp[num+1][length-1]
        elif num == 9:
            dp[num][length] = dp[num-1][length-1]
        else:
            dp[num][length] = dp[num+1][length-1] + dp[num-1][length-1]

 

최종 풀이

dp[i][j] = i라는 숫자로 시작하는 길이 j의 숫자 개수

길이 j가 1일때 모두 1로 초기화 후 n까지 차례로 길이 늘려가며 진행

시작하는 숫자 i가 0, 9, 그외 일때 3가지로 나누어서 진행

import sys
n = int(sys.stdin.readline())
dp = [[0]*(n+1) for _ in range(10)]

for i in range(0,10):
    dp[i][1] = 1

for length in range(2,n+1):
    for num in range(0,10):
        if num == 0:
            dp[num][length] = dp[num+1][length-1]
        elif num == 9:
            dp[num][length] = dp[num-1][length-1]
        else:
            dp[num][length] = dp[num+1][length-1] + dp[num-1][length-1]

total = 0
for i in range(1,10):
    total += dp[i][n]
print(total%1000000000)

 

 

백준 11053 : 가장 긴 증가하는 부분 수열

 

11053번: 가장 긴 증가하는 부분 수열

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오. 예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이

www.acmicpc.net

순서 내용
1. 손으로 그려보며 반복되는 부분 찾기 arr = 10 20 10 30 20 50
10 20 30 50 을 취하면 됨
30과 50을 취할때도 앞의 결과 (10 20)는 바뀌지 않음.
2. dp테이블 dp[i] 의미 정의 i번째 까지 취할 수 있는 가장 긴 수열의 길이
3. 중복되는 부분을 어떻게 활용할 수 있을지 생각해봅니다.
1부터 n까지 순서대로 하나씩 자신의 이전 값 까지만 검사.
가장 긴 수열의 길이를 업데이트하고 이 값을 그 다음에도 활용.
4. 점화식 디테일하게 세우기 for cur in range(1, n):
    for front in range(0, cur):
        if arr[front] < arr[cur]:
            dp[cur] = max(dp[cur], dp[front]+1)

 

최종 풀이

dp[i] = i번째 까지 취할 수 있는 가장 긴 수열의 길이

dp = [1]*n : 처음에 모두 다 길이 1(자기자신)의 수열이라고 보고 1로 초기화 

1부터 n까지 순서대로 진행하며 자신의 이전 값 중에서 자신보다 작은 가장긴 수열길이 + 1로 개신
이 값을 그 다음 순서에서도 활용가능.

import sys
n = int(sys.stdin.readline())
arr = list(map(int, sys.stdin.readline().split()))
dp = [1]*n
for cur in range(1, n):
    for front in range(0, cur):
        if arr[front] < arr[cur]:
            dp[cur] = max(dp[cur], dp[front]+1)
print(max(dp))

 

백준 9251 : LCS

 

9251번: LCS

LCS(Longest Common Subsequence, 최장 공통 부분 수열)문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중 가장 긴 것을 찾는 문제이다. 예를 들어, ACAYKP와 CAPCAK의 LCS는 ACAK가 된다.

www.acmicpc.net

최초 접근법 (풀이아님)

이 문제를 처음에는 가장 긴 증가하는 부분수열 (LIS)와 비슷하게 접근했다.
2번 배열의 각 요소가 1번 배열에서 몇 번째 인덱스인지 기록하고, LIS처럼 풀어버리면 되지 않을까 생각했다. 문제는 같은 알파벳이 여러 번 등장하는 경우였다.
index 012345
arr1  ACAYKP  
arr2  CAPCAK
에서 105124, 105104, 125104, 125124 와 같은 여러 가지 경우가 나와버린다.
사람 머리속으로 생각하면 105124가 나와서 0124 순서로 이으면 답이 4가 나오는데
이를 처리하는데 또 불필요한 추가 연산이 들어가는게 영 아닌것 같아서 중단했다.

 

최종 풀이

dp[i][j] = arr1[:i+1] 과 arr2[:j+1] 의 LCS 길이

1번 리스트의 i번까지의 문자열과

2번 리스트의 j번까지의 문자열의 공통되는 부분 수열의 최장 길이

시도 결과
A → i
C → j
공통 부분 수열 못만든다.
A  → i
CA→ j
A로 1개 만들 수 있다.
AC
CA
CA로 2개 만들 수 있다.
arr1[i] == arr2[j] 이면 길이 1이 추가됨을 알 수 있다.
ACA
CAP
arr1[i] != arr2[j]이면?
이전 문자열로 만들 수 있던 최대 길이를 가지면 된다.
왜냐면 CA로 2개를 만들 수 있다는 사실은 변함이 없다.
CAP
ACA
arr1[i] != arr2[j]인데 이런 경우도 있을 수 있다.
[i-1][j] 와 [i][j-1] 중에서 큰것을 취해야 한다.


arr1[i] != arr2[j] 일땐 max(dp[i-1][j], dp[i][j-1]) 값을 가져야 한다.
import sys
arr1 = ' ' + sys.stdin.readline().rstrip()
arr2 = ' ' + sys.stdin.readline().rstrip()

a_len = len(arr1)
b_len = len(arr2)
dp = [[0] * (b_len) for _ in range(a_len)]

for i in range(1, a_len):
    for j in range(1, b_len): 
        if arr1[i] == arr2[j]: 
            dp[i][j] = dp[i-1][j-1] + 1 
        else: 
            dp[i][j] = max(dp[i-1][j], dp[i][j-1]) 
print(dp[a_len-1][b_len-1])

 

 

백준 12865 : 평범한 배낭

 

12865번: 평범한 배낭

첫 줄에 물품의 수 N(1 ≤ N ≤ 100)과 준서가 버틸 수 있는 무게 K(1 ≤ K ≤ 100,000)가 주어진다. 두 번째 줄부터 N개의 줄에 거쳐 각 물건의 무게 W(1 ≤ W ≤ 100,000)와 해당 물건의 가치 V(0 ≤ V ≤ 1,000)

www.acmicpc.net

순서 내용
1. 손으로 그려보며 반복되는 부분 찾기 n=4, k=7
6 13 → 무게가 6이되어 더이상 추가불가
5 12 → 무게가 5이므로 더이상 추가불가
4 8 → 무게가 4이므로 최대 3까지 추가 가능
3 6 → 무게 여유가 있어서 추가 가능
4 10 → ???

(4,8), (3,6) → (7,14)
(3 6), (4 10) → 이렇게 택하면 (7,16) 으로 최대가 나온다.
2. dp테이블 dp[i] 의미 정의 i번째 까지 물건을 택했을때의 최대 가치
3. 무게를 어떻게 저장 할 것인가?
선택지는 두가지다.
i번 물건을
가방에
1) 넣는다
2) 안 넣는다.

→ 백트래킹으로도 풀 수 있을 것으로 보인다.

하지만 무게는 어떻게 저장할 것인지가 고민이다.
4. dp 테이블 확장하기
dp[i] → dp[i][j]
i번에서 j번까지 물건을 택했을때의 최대 가치
5. 확장된 테이블로 시뮬레이션 도저히 시뮬레이션 불가. 포기
6. dp 테이블 재정의
dp[i][w]
i번째 까지 물건을 택했을때의 최대 가치
이 때의 무게를 w라고 정의
7. 점화식 디테일하게 세우기 1) i번 물건 택한 경우
dp[i][w] = 
max(dp[i-1][w],dp[i-1][w-arr[i][0]] + arr[i][1]) 

2) i번 물건 택하지 않은 경우
dp[i][w] = dp[i-1][w]

 

최종 풀이

dp[i][w] = i번째 까지 물건 취했을때 무게w로 만들 수 있는 최대 가치


dp[i] 라는것은 i번 까지의 물건을 택했을때의 최대가치다.

for 물건 i를 택했을때

for 무게를 1부터 최대한도까지 차례로 증가시켜본다.

  i번 물건의 무게가 더 무거우면 i-1번 까지 택했을때 최대가치를 저장

  = dp[i-1][w]

 

  i번 물건의 무게가 크거나 같으면 가방에 담을 수 있다.

  이때 담을지 안 담을지 선택이 가능하다. 가치가 큰 쪽을 선택.

  1) i를 담지 않는다. = dp[i-1][w] 

  2) i를 담는다. = dp[i-1][w-arr[i][0]] + arr[i][1]

     → 현재 무게를 빼줘야 담아진다. 이런 생각이 제일 어려운것 같다. ㅠㅠ

import sys
n,k = map(int, sys.stdin.readline().split())
arr = [(0,0)]+[tuple(map(int, sys.stdin.readline().split())) for _ in range(n)]
dp = [[0]*(k+1) for _ in range(n+1)]

for i in range(1,n+1):
    for w in range(1,k+1):
        if w >= arr[i][0]:
            dp[i][w] = max(dp[i-1][w],dp[i-1][w-arr[i][0]] + arr[i][1]) # 택한 경우
        else : 
            dp[i][w] = dp[i-1][w] # 택하지 않는 경우
print(dp[n][k])

 

반응형
저작자표시 동일조건

'Python > 알고리즘' 카테고리의 다른 글

파이썬(python) 투 포인터  (0) 2021.05.13
파이썬(python) 알고리즘 - 최단 경로 (다익스트라)  (0) 2021.05.09
파이썬(python) 알고리즘 - DFS, BFS  (0) 2021.04.22
파이썬 (python) 알고리즘 - 그리디 알고리즘  (0) 2021.04.22
파이썬(python) 알고리즘 - 소수 찾기  (0) 2021.04.18
    'Python/알고리즘' 카테고리의 다른 글
    • 파이썬(python) 투 포인터
    • 파이썬(python) 알고리즘 - 최단 경로 (다익스트라)
    • 파이썬(python) 알고리즘 - DFS, BFS
    • 파이썬 (python) 알고리즘 - 그리디 알고리즘
    snowman95
    snowman95
    (17~19) Unity/Unreal Engine 게임 프로그래머 (20~21) System Administrator ___________ (22~) React 웹 프론트앤드 개발자 __________ 깃헙 : https://github.com/snowman95

    티스토리툴바