[Algorithm] Programmers : 거스름돈 by Python

엄희관·2021년 1월 9일
0

Algorithm

목록 보기
55/128
post-thumbnail

[문제 바로가기] https://programmers.co.kr/learn/courses/30/lessons/12907

📌문제 설명

Finn은 편의점에서 야간 아르바이트를 하고 있습니다. 야간에 손님이 너무 없어 심심한 Finn은 손님들께 거스름돈을 n 원을 줄 때 방법의 경우의 수를 구하기로 하였습니다.

예를 들어서 손님께 5원을 거슬러 줘야 하고 1원, 2원, 5원이 있다면 다음과 같이 4가지 방법으로 5원을 거슬러 줄 수 있습니다.

  • 1원을 5개 사용해서 거슬러 준다.
  • 1원을 3개 사용하고, 2원을 1개 사용해서 거슬러 준다.
  • 1원을 1개 사용하고, 2원을 2개 사용해서 거슬러 준다.
  • 5원을 1개 사용해서 거슬러 준다.

거슬러 줘야 하는 금액 n과 Finn이 현재 보유하고 있는 돈의 종류 money가 매개변수로 주어질 때, Finn이 n 원을 거슬러 줄 방법의 수를 return 하도록 solution 함수를 완성해 주세요.

제한 사항

  • n은 100,000 이하의 자연수입니다.
  • 화폐 단위는 100종류 이하입니다.
  • 모든 화폐는 무한하게 있다고 가정합니다.
  • 정답이 커질 수 있으니, 1,000,000,007로 나눈 나머지를 return 해주세요.

입출력 예


💡 문제 풀이

이전에 비슷한 문제들을 풀어본 경험이 있어서 바로 DP(Dynamic Programming)로 풀어야겠다고 생각했다.

문제의 관건은 중복된 조합은 배제해야한다는 것이다.
ex) 1+2+2, 2+1+2, 2+2+1을 모두 같은 경우로 봐야 한다.

따라서 처음에는 (1)숫자, (2)조합의 개수를 판단하기 위한 두 개의 deque를 만들었다.

  • queue_num : 잔돈의 총합을 담는 queue
  • queue_used : 사용한 잔돈의 개수를 알려주는 리스트를 담은 queue

queue_num의 원소가 존재할 때 까지 아래의 과정을 반복했다.

  1. queue_num의 숫자를 하나씩 빼내어(popleft), money에 담겨있는 잔돈들을 차례대로 더한다.

  2. 동시에 queue_used에서도 리스트를 하나씩 빼내어(popleft) 사용한 잔돈을 기록한다.

  3. 만약 더한 돈이 n보다 같거나 작고 잔돈을 사용한 내역이 이전에 존재하지 않으면(not in queue_used) 추가한 내용을 다시 queue에 각각 담는다.
    ※ 같거나 작은 범위로 설정한 이유는 n과 같을 때의 temp도 queue_used에 담아 이후 중복된 조합이 나타나지 않게하기 위함이다.

  4. 3번과정에서 만약 n과 같다면 answer+1을 해준다.

코드는 아래와 같다.

테스트케이스 통과 & 효율성 실패

from collections import deque
def solution(n, money):
    answer = 0
    queue_num = deque([0])
    queue_used = deque([[0] * len(money)])
    while queue_num:
        num = queue_num.popleft()
        used = queue_used.popleft()
        for idx in range(len(money)):
            temp = used[::]
            temp[idx] += 1
            if num + money[idx] <= n and temp not in queue_used:
                queue_num.append(num + money[idx])
                queue_used.append(temp)
                if num + money[idx] == n:
                    answer += 1
    return answer

나름 코드는 최적화했다고 생각했는데... 효율성에서 모두 '실패'처리 되었다.
불필요한 변수 혹은 방법을 변경해도 효율성을 통과하지 못했다.

다른 사람의 풀이를 보면서 DP를 좀 더 공부한 후 해결하였다.

DP를 풀기 위해서는 기본적으로 DP 과정을 테이블화 시킬 줄 알아야 한다는 것을 배웠다.

문제를 해결하기위해 테이블로 DP를 나타내었다.

처음에는 위 테이블 조차 이해가가지 않았는데 이해했을 때는 신기했다 😮

잔돈(money)으로 n값을 표현할 수 있는 경우의 수를 찾으려면
n이하 값의 자연수에 대해서 잔돈(money)으로 표현할 수 있는 경우의 수를 찾아 DP 방식으로 차곡차곡 쌓아나가면 된다.

따라서 x축은 n, y축은 money의 종류를 나타내어 테이블을 세팅한다.

하나의 행(잔돈)마다 좌측에서 우측으로 테이블 값을 채워나간다.

테이블을 이해하기 위해서는 아래의 코드를 이해하는 것이 가장 중요하다.

if c >= money[r]:
    table[r][c] = (table[r-1][c] + table[r][c-money[r]]) % 1000000007
else:
    table[r][c] = table[r-1][c]

만약 money >= n이게 되면 해당 money로 n을 만들 수 있다는 뜻이기 때문에

  1. 현재 money를 사용하지 않았을 때 n을 만들 수 있는 경우 : table[r-1][c]
  2. money를 더하기 직전의 값(n-money[r])에서 현재 잔돈(money)을 추가하는 경우 : table[r]c-money[r]]

위 두 개의 값을 합해준다.

만약, money < n이면 해당 money를 사용했을 때 n보다 커지기 때문에 만들 수 없으므로 단순히 table[r-1][c] 값만 취한다.

ex) n이 2이고 money가 2인 경우 (2, 2)를 채워야 한다.

먼저, money(2)를 이용하지 않고 n을 만들 수 있는 경우의 수 table[1][2] = 1
다음으로, money(2)를 추가하기 전의 경우 table[2][0] = 1
위 두 값을 더해서 table[2][2]는 2가 된다.

정답

def solution(n, money):
    table = [[0] * (n+1) for _ in range(len(money))]
    table[0][0] = 1
    for i in range(money[0], n+1, money[0]):
        table[0][i] = 1
    for r in range(1, len(money)):
        for c in range(n+1):
            if c >= money[r]:
                table[r][c] = (table[r-1][c] + table[r][c-money[r]]) % 1000000007
            else:
                table[r][c] = table[r-1][c]
    return table[-1][-1]

DP를 제대로 풀기위해서는 손코딩 연습을 좀 더 해야겠다.(for 테이블만들기)

profile
허브

0개의 댓글