[프로그래머스 3단계] 알고리즘 43. 거스름돈
문제 출처는 프로그래머스 알고리즘 연습 에서 볼 수 있습니다!(https://programmers.co.kr/learn/challenges) 2018년 5월 중순 프로그래머스 홈페이지가 업데이트가 되었습니다. 기존 알고리즘 단계 분류가 틀리니 참고하세요.
알고리즘 43. 거스름돈
코드
풀이:
이 문제는 전형적인 DP 문제이다. 먼저 간단한 기본 DP 부터 살펴보자. DP의 주된 아이디어는 다음과 같다.
- 2차원 배열을 만든다
- 각 열은 총 금액이다.
- 각 행은 총 금액을 낼 수 있는 경우의 수이다.
- 만약 1, 2, 5라면 각 행의 경우의 수는 다음과 같다.
- 첫 행은 1원으로 낼 수 있는 경우의 수.
- 둘째 행은 1, 2원으로 낼 수 있는 경우의 수
- 셋째 행은 1, 2, 5원으로 낼 수 있는 경우의 수이다.
나의 코드는 다음과 같다.
여기서 중요한 점은 이거다. 만약 j원에 대해서 1, 2, 5에서 낼 수 있는 경우의 수를 검사해보자. 이전에 1, 2를 검사한 행은 dp[i-1][j]일 것이다. 그것을 현재 dp[i][j]에 저장한다. 그 후 5원짜리가 1 개일때, 2개 일때 ... j / 5가 될때까지 검사를 한다. 테이블로 한번 나타내 보자.
|
0 |
1 |
2 |
3 |
4 |
5 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
2 (1, 2로 표현) |
1(1로 표현) + 0 |
1 + 0 |
1 + 1 = 2 |
1 + 1 |
1 + 2 |
1 + 2 |
5 (1, 2, 5) |
1 + 0 |
1 + 0 |
2 + 0 |
2 + 0 |
2 + 0 |
3 + 1 |
n=5일 때 1를 검사할 때 경우의 수를 살펴보자.
1 5개 1 1 1 1 1 = dp[0][5] = 1
총 1가지이다.
n=5일 때 1, 2를 검사할 때 경우의 수를 살펴보자.
2원 0개 1 1 1 1 1 = dp [1][5] = dp[0][5 - (2*0)]
---------------------------- 여기까지가 1로 검사했던 경우의 수이다.
2원 1개1 1 1 2 = dp[1][5] += dp[0][5-(2*1)]
2원 2개 1 2 2 = dp[1][5] += dp[0][5-(2*2)]
그래서 총 3가지이다.
이번엔 1, 2, 5로 검사해보자.
5원 0개일 때 = dp[2][5] = dp[1][5-(5*0)]
1 1 1 1 1
1 1 1 2
1 2 2
------------------------(여기까지가 1, 2로 검사했던 경우의 수다.)
5원 1개 = dp[2][5] = dp[1][5-(5*1)]
5
그래서 총 4가지이다.
나의 DP 아이디어를 이해했는가? money[i]에 대해 검사하고 있을 때 그 동전이 0개일 때는 이전의 검사했던 경우의 수이다. 1개씩 늘어날때마다 이전 결과에서 [j-(money[i] * 동전수)] 인덱스의 경우의 수를 나타낸다.
그러나 문제점!
그런데 이 코드를 실행하면 효율성 검사에서 시간초과가 나온다. (필자 같은 경우 이 문제를 1주일동안 고민했었다.) 그러던 중 dp를 효율적으로 계산할 방법이 떠올랐다. 기본적으로 위의 아이디어는 최악의 시간복잡도가 n의 3제곱이다. 내가 개선할 아이디어는 n의 제곱으로 문제를 풀 수 있다. 일단 테이블을 살펴보자. 예를 일반화해서 보기 위해 입력값을 좀 크게 해보았다. n=11 money = {2, 3, 5, 7}이다. 그러면 테이블은 다음과 같다.
|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
2 |
1 |
0 |
1 |
0 |
1 |
0 |
1 |
0 |
1 |
0 |
1 |
0 |
3 |
1 |
0 |
1 |
1 |
1 |
1 |
2 |
1 |
2 |
2 |
2 |
2 |
5 |
1 |
0 |
1 |
1 |
1 |
2 |
2 |
2 |
2 |
3 |
4 |
4 |
7 |
1 |
0 |
1 |
1 |
1 |
2 |
2 |
3 |
2 |
4 |
5 |
5 |
우선 첫 행은 기존 아이디어와 같다. 이제부터 주의깊게 살펴보자. 먼저 3일 때이다.
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
3 | 1 | 0 | 1 | 1 | 1 | 1 | 2 | 1 | 2 | 2 | 2 | 2 |
5 | 1 | 0 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 3 | 4 | 4 |
7 | 1 | 0 | 1 | 1 | 1 | 2 | 2 | 3 | 2 | 4 | 5 | 5 |
이 색칠한 테이블의 공통점이 무엇인지 아는가? 나의 아이디어의 힌트이다! 정답은 바로 이것이다.
dp[i][j] = dp[i-1][j] + dp[i][j-3]
그럼 5를 한번 살펴보자.
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
3 | 1 | 0 | 1 | 1 | 1 | 1 | 2 | 1 | 2 | 2 | 2 | 2 |
5 | 1 | 0 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 3 | 4 | 4 |
7 | 1 | 0 | 1 | 1 | 1 | 2 | 2 | 3 | 2 | 4 | 5 | 5 |
5 역시 다음과 같은 규칙을 따른다.
dp[i][j] = dp[i-1][j] + dp[i][j-5]
이번엔 7을 살펴보자.
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
3 | 1 | 0 | 1 | 1 | 1 | 1 | 2 | 1 | 2 | 2 | 2 | 2 |
5 | 1 | 0 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 3 | 4 | 4 |
7 | 1 | 0 | 1 | 1 | 1 | 2 | 2 | 3 | 2 | 4 | 5 | 5 |
7 역시 이런 규칙을 따른다.
dp[i][j] = dp[i-1][j] + dp[i][j-7]
그렇다면 점화식을 다음과 같이 쓸 수 있다.
j >= money[i]
dp[i][j] = dp[i-1][j - money[i]];
else
dp[i][j] = dp[i-1][j];
나의 총 코드는 다음과 같다.
약간의 설명을 덧붙이자면 나는 1차원 배열로 풀었다. 왜냐하면 어차피 j >= money[i] 일때만 값이 추가되기 때문에 그 이전에는 검사할 필요가 없다고 판단해서이다. 진짜 이번 문제는 기특하게 풀었다.