06-2. 다이나믹 프로그래밍 문제풀이

ji-vvon·2021년 8월 3일
2

알고리즘(파이썬)

목록 보기
32/41

📝문제1. 1로 만들기

- 문제

정수 X가 주어질때 정수 X에 사용할 수 있는 연산은 다음과 같이 4가지이다.

a. x가 5로 나누어떨어지면, 5로 나눈다.

b. X가 3으로 나누어 떨어지면, 3으로 나눈다.

c. X가 2로 나누어 떨어지면, 2로 나눈다.

d. X에서 1을 뺀다.

정수 X가 주어졌을때, 연산 4개를 적절히 사용해서 1을 만들려고 한다. 이 연산을 사용하는 횟수의 최솟값을 출력하시오.

예를 들어 정수가 26이면 다음과 같이 계산해서 3번의 연산이 최솟값이다.
1. 26 - 1 = 25 (d)
2. 25 / 5 = 5 (a)
3. 5 / 5 = 1 (a)

입력
첫째 줄에 정수 X이 주어진다. (1<=X<=30,000)
출력
첫째 줄에 연산을 하는 횟수의 최솟값을 출력한다.

입력 예시
26
출력 예시
3

- 나의 코드💻

from sys import stdin
x = int(stdin.readline())
count = 0

while x != 1:
    if x % 5 == 0:
        x = x // 5
        count += 1
    elif x % 3 == 0:
        x = x // 3
        count += 1
    elif x % 2 == 0:
        x = x // 2
        count += 1
    else:
        x -= 1
        count += 1

print(count)

-> 틀렸다. 너무 생각없이 작성한듯..

- 코드💻

from sys import stdin
x = int(stdin.readline())

# 앞서 계산된 결과를 저장하기 위한 DP 테이블 초기화
d = [0] * 3001

# 다이나믹 프로그래밍 진행(보텀업)
for i in range(2, x+1):
    # 현재의 수에서 1을 빼는 경우
    d[i] = d[i-1] + 1
    # 현재의 수가 2로 나누어 떨어지는 경우
    if i % 2 == 0:
        d[i] = min(d[i], d[i // 2] + 1)
    # 현재의 수가 3으로 나누어 떨어지는 경우
    if i % 3 == 0:
        d[i] = min(d[i], d[i // 3] + 1)
    # 현재의 수가 5로 나누어 떨어지는 경우
    if i % 5 == 0:
        d[i] = min(d[i], d[i // 5] + 1)

print(d[x])

...

- 해설📖

  • 다이나믹 프로그래밍 문제이다. (보텀업 방식, 피보나치와 비슷함)
    *보텀업 : 반복문을 이용해 작은 문제부터 차근차근 답을 도출
  • 점화식 끝에 1을 더해주는 이유는 함수의 호출 횟수를 구해야 하기 때문이다.
  • 1을 빼는 연산을 제외하고는 해당 수로 나누어떨어질 때에 한해서만 점화식을 적용할 수 있다. 더불어 두 수 중에서 단순히 더 작은 수를 구하고자 할 때는 파이썬의 min() 함수를 이용하면 간단하다.

이해가 잘 안가서 x = 6 인 경우를 가정해 코드 속 내용을 직접 작성해보았다.

[반복문]
1) i = 2
d[0], d[1] = 0, 0
d[2] = d[1] + 1 = 1 이다.

2는 2로 나누어 떨어지므로
d[2] = min(d[2], d[2//2]+1) = min(1, 1) = 1이 된다.

2) i = 3
d[3] = d[2] + 1 = 2
3은 3으로 나누어 떨어지므로
d[3] = min(d[3], d[3//3]+1) = min(2, 1) = 1이 된다.

3) i = 4
d[4] = d[3] + 1 = 2
4는 2, 3, 5로 나누어 떨어지지 않으므로 d[4] = 2이다.

4) i = 5
d[5] = d[4] + 1 = 3
5는 5로 나누어 떨어지므로
d[5] = min(d[5], d[5//5] + 1) = min(3, 1) = 1이 된다.

5) i = 6
d[6] = d[5] + 1 = 2
6은 2로 나누어 떨어지므로
d[6] = min(d[6], d[6//2] + 1) = min(d[6], d[3]+1) = min(2, 2) = 2
6은 3으로 나누어 떨어지므로
d[6] = min(d[6], d[6//3] + 1) = min(d[6], d[2]+1) = min(2, 2) = 2

-> x인 경우 답은 2가 된다.


📝문제2. 개미 전사

- 문제

개미전사는 부족한 식량을 충당하고자 메뚜기 마을의 식량창고를 몰래 공격하려고 한다. 메뚜기 마을에는 여러 개의 식량창고가 있는데 식량창고는 일직선으로 이어져 있다. 각 식량창고에는 정해진 수의 식량을 저장하고 있으며 개미 전사는 식량창고를 선택적으로 약탈하여 식량을 빼앗을 예정이다. 이때 메뚜기 정찰병들은 일직선상에 존재하는 식량창고 중에서 서로 인접한 식량창고가 공격받으면 바로 알아챌 수 있다. 따라서 개미 전사가 정찰병에게 들키지 않고 식량창고를 약탈하기 위해서는 최소한 한 칸 이상 떨어진 식량창고를 약탈해야 한다. 예를 들어 식량창고 4개가 다음과 같이 존재한다고 가정하자.

{1, 3, 1, 5}

이때 개미 전사는 두 번째 식량창고와 네 번째 식량창고를 선택했을 때 최댓값인 총 8개의 식량을 빼앗을 수 있다. 개미 전사는 식량창고가 이렇게 일직선상일 때 최대한 많은 식량을 얻기를 원한다.

개미 전사를 위해 식량창고 N개에 대한 정보가 주어졌을 때 얻을 수 있는 식량의 최댓값을 구하는 프로그램을 작성하시오.

입력

  • 첫째 줄에 식량창고의 개수 N이 주어진다. (3<=N<=100)
  • 둘째 줄에 공백으로 구분되어 각 식량창고에 저장된 식량의 개수 K가 주어진다. (0<=K<=1,000)

출력

  • 첫째 줄에 개미 전사가 얻을 수 있는 식량의 최댓값을 출력하시오.

입력 예시
4
1 3 1 5

출력 예시
8

- 코드💻

n = int(input())
array = list(map(int, input().split()))

# dp 테이블 초기화
d = [0] * 100

# 다이나믹 프로그래밍 진행(보텀업)
d[0] = array[0]
d[1] = max(array[0], array[1])
for i in range(2, n):
    d[i] = max(d[i-1], d[i-2] + array[i])

print(d[n-1])

- 해설📖

왼쪽부터 차례대로 식량 창고를 턴다고 가정하면 된다. 왼쪽부터 차례대로 식량창고를 털지 안 털지를 결정하는 경우와 특정한 i번째 식량창고에 대해서 털지 안털지의 여부를 결정할 때, 단 2가지 경우에 대해서만 확인하면 된다.

a. (i-1)번째 식량창고를 털기로 결정한 경우 현재의 식량창고를 털 수 없다.
b. (i-2)번째 식량창고를 털기로 결정한 경우 현재의 식량창고를 털 수 있다.

따라서 a와 b 중에서 더 많은 식량을 털 수 있는 경우를 선택하면 된다.

여기서 알아둘 점은 왼쪽부터 (i-3)번째 이하의 식량창고에 대해서는 고려할 필요가 없다. 왜냐하면 한 칸 이상 떨어진 식량창고는 항상 털 수 있기 때문이다.


📝문제3. 바닥 공사

- 문제

가로의 길이가 N, 세로의 길이가 2인 직사각형 형태의 얇은 바닥이 있다. 태일이는 이 얇은 바닥을 1 X 2의 덮개, 2 X 1의 덮개, 2 X 2의 덮개를 이용해 채우고자 한다.

이 때 바닥을 채우는 모든 경우의 수를 구하는 프로그램을 작성하시오.

입력
첫째 줄에 N이 주어진다. (1 ≤ N ≤ 1,000)

출력
첫째 줄에 2 X N 크기의 바닥을 채우는 방법의 수를 796,796으로 나눈 나머지를 출력한다.

입력 예시
3

출력 예시
5

- 코드💻

n = int(input())

d = [0] * 1001

d[1] = 1
d[2] = 3
for i in range(3, n+1):
    d[i] = (d[i-1] + 2 * d[i-2]) % 796796
    
print(d[n])

- 해설📖

다이나믹 프로그래밍의 타일링 문제 유형이다.
다이나믹 프로그래밍에서는 종종 결과를 어떤 수로 나눈 결과를 출력하라는 내용이 들어가는데, 이는 단지 결괏값이 굉장히 커질 수 있기 때문에 그런 것이다. 따라서 값을 계산할 때마다 특정한 수로 나눈 나머지만 취하도록 하면 된다.

이 문제는 그림으로 그려 생각하면 어렵지 않게 풀 수 있다. 예를 들어 N이 3일 때 바닥을 덮기로 채울 수 있는 모든 경우의 수는 다음과 같다.

또한 왼쪽부터 차례대로 바닥을 덮개로 채운다고 생각하면 어렵지 않게 점화식을 세울 수 있다.

  1. 왼쪽부터 i-1까지 길이가 덮개로 이미 채워져 있으면 2x1의 덮개를 채우는 하나의 경우밖에 존재하지 않는다.
  2. 왼쪽부터 i-2까지 길이가 덮개로 채워져 있으면 1x2덮개 2개를 넣는 경우, 혹은 2x2의 덮개 하나를 넣는 경우로 2가지 경우가 존재한다.

    또한 이 문제 역시 왼쪽부터 N-2 미만의 길이에 대해서는 고려할 필요가 없다. 왜냐하면 사용할 수 있는 덮개의 형태가 최대 2x2의 직사각형 형태이기 때문이다. 다시 말해 바닥을 채울 수 있는 형태는 위에서 언급한 경우밖에 없다.

📝문제4. 효율적인 화폐 구성

- 문제

- 코드💻

# 정수 N, M을 입력 받기
n, m = map(int, input().split())
# N개의 화폐 단위 정보를 입력 받기
array = []
for i in range(n):
    array.append(int(input()))

# 한 번 계산된 결과를 저장하기 위한 DP 테이블 초기화
d = [10001] * (m + 1)

# 다이나믹 프로그래밍(Dynamic Programming) 진행(보텀업)
d[0] = 0
for i in range(n):
    for j in range(array[i], m + 1):
        if d[j - array[i]] != 10001: # (i - k)원을 만드는 방법이 존재하는 경우
            d[j] = min(d[j], d[j - array[i]] + 1)

# 계산된 결과 출력
if d[m] == 10001: # 최종적으로 M원을 만드는 방법이 없는 경우
    print(-1)
else:
    print(d[m])

- 해설📖


내 생각엔 문제 해결 아이디어를 차근차근 떠올려보고 점화식을 세우는 것이 중요한 것 같다.

모든 내용은 '이것이 코딩 테스트다 with 파이썬(나동빈)' 책과 유튜브 강의를 기반으로 작성한 글입니다.

2개의 댓글

comment-user-thumbnail
2021년 8월 3일

안녕하세요, 김덕우입니다! 이번 내용이 유독 이해하기 어려웠는데, 상세히 설명해주셔서 웃음님 포스팅 보고 다시 공부해봐야겠어요. 수고 많으셨습니다!!!!!!

답글 달기
comment-user-thumbnail
2021년 8월 4일

안녕하세요 알고리줌입니다.!
저도 다이나믹에서는 점화식을 세우는게 가장 중요하다고 생각했습니다.!
오늘 수고많으셨습니다.

답글 달기