문제 이해

숫자 배열에 들어있는 값으로 target 숫자 만들면 되는 문제

카테고리는 DFS/BFS로 되어있긴 한데, DP로 풀어도 될 듯 ??

오랜만에 DP로 풀어보겠다.

풀이

오랜만에 dp 알고리즘을 다루는 것이기 때문에 대략 이론적인 부분도 정리하겠음

일단 dp 문제를 풀 때 가장 중요하게 봐야할 건 아래 세 가지임

1. 상태를 무엇으로 표현할 것인가 ?
2. 이전 상태에세 다음 상태를 어떻게 만들 것인가 ?
3. 초기값은 무엇인가 ?

이 문제 (숫자 몇 개를 사용해서 특정 합을 만들 수 있는 경우의 수)에서는 상태를

dp[i][sum]

으로 볼 수 있다.

i는 몇 번째 숫자까지 사용했는지, sum은 현재까지 만들어진 합

예를 들면

dp[3][5] = 2

이거는 앞에서 3개의 숫자를 사용했을 때, 합이 5가 되는 방법이 두 가지 있다 라는 뜻임

이게 주관적으로 dp에서 가장 중요하다고 생각하는 상태 정의임

그리고 만약 다음 숫자가 추가로 더 있으면, 그 숫자로 인해 다음 합이 만들어질 것임

그럼 아래와 같이 표현할 수 있음

dp[3][5] -> dp[i][n]

이렇게 현재 상태에서 다음 상태로 값을 전달하는데, 이걸 점화식이라고 함


dp에는 크게 두 가지 방식이 있음

1. Top-Down

탑다운은 재귀 + 메모이제이션을 사용하는 방식임

필요한 값을 재귀적으로 계산 -> 한 번 계산한 값은 배열에 저장 -> 다시 필요하면 저장된 값 사용

이렇게 말로 정리해놓으니까 DFS랑 비슷하게 생긴 듯?

2. Bottom-Up

바텀업은 작은 상태부터 차례대로 채우는 방식

구글에 검색한 결과 “가장 작은 하위 문제부터 차례대로 답을 계산해 테이블을 채워나가는 방식” 이라고 함

보통 반복문을 사용


이 문제에서는 바텀업 방식이 적절해보인다.

그럼 일단 차근차근 생각을 해보자.

배열이 [1, 1, 1, 1, 1], target이 3이라고 가정해보자.

그렇다면 초기 상태를 어떻게 둘 것인가 ?

아무 숫자도 사용하지 않을 때의 합은 몇이고, 그 경우의 수는 몇 개인가 ?

아무 숫자도 사용하지 않을 때의 합은 0이고, 그 경우의 수는 1이다. (아무 것도 사용하지 않는 경우)

그럼 dp[0][0] = 1 로 볼 수 있음

다음 dp[1]은

0 + 1 = 1
0 - 1 = -1

이기 때문에

dp[1][1] = 1
-> 숫자 한 개를 사용해서 합 1을 만드는 방법이 1개

dp[1][-1] = 1
-> 숫자 한 개를 사용해서 합 -1르 만드는 방법이 1개

dp[2]는

1 + 1 = 2
1 - 1 = 0
-1 + 1 = 0
-1 - 1 = -2

이기 때문에

dp[2][2] = 1
-> 숫자 두 개를 사용해서 합 2를 만드는 방법이 1개

dp[2][0] = 2
-> 숫자 두 개를 사용해서 합 0을 만드는 방법이 2개

dp[2][-2] = 1
-> 숫자 두 개를 사용해서 합 -2를 만드는 방법이 1개

dp[3]은

2 + 1 = 3
2 - 1 = 1
0 + 1 = 1
0 - 1 = -1
0 + 1 = 1
0 - 1 = -1
-2 + 1 = -1
-2 - 1 = -3

이기 때문에

dp[3][3] = 1
dp[3][1] = 3
dp[3][-1] = 3
dp[3][-3] = 1

dp[4]

3 + 1 = 4
3 - 1 = -2

---

1 + 1 = 2
1 - 1 = 0

x3

---

-1 + 1 = 0
-1 - 1 = -2

x3

---

-3 + 1 = -2
-3 - 1 = -4
dp[4][4] = 1
dp[4][2] = 3
dp[4][0] = 6
dp[4][-2] = 5
dp[4][-4] = 1

dp[5]

4 + 1 = 5
4 - 1 = 3
---
2 + 1 = 3
2 - 1 = 1

x3
---
0 + 1 = 1
0 - 1 = -1

x6
---
-2 + 1 = -1
-2 - 1 = -3

x5
---
-4 + 1 = -3
-4 - 1 = -5
dp[5][5] = 1
dp[5][3] = 4
dp[5][1] = 9
dp[5][-1] = 11
dp[5][-3] = 6
dp[5][-5] = 1

이렇게 될 것임. 그럼 3이 나오는 경우는 총 5가지

노가다긴 했지만 이렇게 작성함으로써

서로 다른 경로가 같은 sum에 도착하면, 그 경우의 수를 dp에 합쳐 저장한다.

라는 걸 이해하기 위함이었다.

근데 위처럼 이차원 배열로 작성하면 sum에 음수가 올 수 있기 때문에 배열 인덱스로 사용할 수 없음

그래서 Map을 통해 key는 sum, value는 그 sum을 만드는 경우의 수가 오도록 작성하면 될 거 같음

코드

package programmers.P43165;

import java.util.HashMap;
import java.util.Map;

public class Main {

    public static void main(String[] args) {
        int[] n = {4, 1, 2, 1};
        int t = 4;

        System.out.println(solution(n, t));
    }

    public static int solution(int[] numbers, int target) {
        Map<Integer, Integer> dp = new HashMap<>();
        dp.put(0, 1);

        for (int n : numbers) {
            Map<Integer, Integer> next = new HashMap<>();

            for (Map.Entry<Integer, Integer> entry : dp.entrySet()) {
                int sum = entry.getKey();
                int cnt = entry.getValue();

                int plus = sum + n;
                int minus = sum - n;

                next.put(plus, next.getOrDefault(plus, 0) + cnt);
                next.put(minus, next.getOrDefault(minus, 0) + cnt);
            }

            dp = next;
        }

        return dp.get(target);
    }
}

코드를 살펴보자면

일단 Map을 사용해서 dp를 구현했음

상태는 위에서 설명한대로 key는 sum, value는 sum을 만드는 경우의 수로 정의했음

그럼 아무 숫자도 사용하지 않은 최초 상태는 sum = 0, value = 1이 되어야 하기 때문에 0, 1 삽입

sum에 n을 더하거나 빼거나 두 가지 경우를 next라는 Map을 선언해서 삽입해준다

getOrDefault 메서드를 통해 같은 sum이 나오는 경우엔 경우의 수를 누적해줬음