[DP] 단어 퍼즐

지은·2023년 5월 26일
0

Algorithm

목록 보기
27/33

문제

단어 퍼즐은 주어진 단어 조각들을 이용해서 주어진 문장을 완성하는 퍼즐입니다. 이때, 주어진 각 단어 조각들은 각각 무한개씩 있다고 가정합니다. 예를 들어 주어진 단어 조각이 [ba, na, n, a]인 경우 ba, na, n, a 단어 조각이 각각 무한개씩 있습니다.
이때, 만들어야 하는 문장이 banana라면 ba, na, n, a의 4개를 사용하여 문장을 완성할 수 있지만, ba, na, na의 3개만을 사용해도 banana를 완성할 수 있습니다.
사용 가능한 단어 조각들을 담고 있는 배열 strs와 완성해야 하는 문자열 t가 매개변수로 주어질 때, 주어진 문장을 완성하기 위해 사용해야 하는 단어조각 개수의 최솟값을 return 하도록 solution 함수를 완성해 주세요. 만약 주어진 문장을 완성하는 것이 불가능하면 -1을 return 하세요.

제한사항

strs는 사용 가능한 단어 조각들이 들어있는 배열로, 길이는 1 이상 100 이하입니다.
strs의 각 원소는 사용 가능한 단어조각들이 중복 없이 들어있습니다.
사용 가능한 단어 조각들은 문자열 형태이며, 모든 단어 조각의 길이는 1 이상 5 이하입니다.
t는 완성해야 하는 문자열이며 길이는 1 이상 20,000 이하입니다.
모든 문자열은 알파벳 소문자로만 이루어져 있습니다.

입출력 예

strstresult
[ "ba", "na", "n", "a" ]"banana"3
[ "app", "ap", "p", "l", "e", "ple", "pp" ]"apple"2
[ "ba", "an", "nan", "ban", "n" ]"banana"-1

입출력 예 설명

입출력 예 #1

문제의 예시와 같습니다.

입출력 예 #2

ap 1개, ple 1개의 총 2개로 apple을 만들 수 있으므로 필요한 단어 개수의 최솟값은 2를 return 합니다.

입출력 예 #3

주어진 단어로는 banana를 만들 수 없으므로 -1을 return 합니다.


풀이

동적 계획법

먼저 문제를 어떻게 정의할지 정해야한다. 이 문제의 경우 필요한 단어 개수의 최솟값을 구해야하기 때문에, 가장 작은 문제도 단어 개수로 정의하는 것이 좋다.

예를 들어, 입출력 예 1의 banana의 경우 가장 작은 문제를 다음과 같이 정의할 수 있다.

  • 단어 b를 만들 수 있는 단어 개수의 최솟값은?

그 다음 큰 문제는 다음과 같이 정의할 수 있다.

  • 단어 ba를 만들 수 있는 단어 개수의 최솟값은?
  • 단어 ban를 만들 수 있는 단어 개수의 최솟값은?
    ...
  • 단어 banana를 만들 수 있는 단어 개수의 최솟값은?

이렇게 가장 작은 문제를 정의했다면, 작은 문제를 통해 큰 문제의 답을 해결할 수 있는지 확인해야 한다.
단어 ba나 단어 ban의 단어 개수 최솟값을 이전 답을 통해 알 수 있을까?

1. 먼저 단어 b부터 살펴보면,

단어 b의 경우 b를 만들 수 있는 단어 조각이 [ba, na, n, a]에 없기 때문에 불가능하다.
➡️ 따라서 -1이다.

2. 그 다음 단어 ba를 살펴보면,

단어 ba의 경우, [b, a] 또는 [ba]의 조합으로 만들 수 있는데,

  • [b, a]의 경우, b를 만들 수 있는 단어 조각이 [ba, na, n, a]에 없기 때문에 불가능하다.
  • [ba]의 경우, ba를 [ba, na, n, a]에서 단어 조각 ba로 만들 수 있다.

➡️ 따라서 ba는 1개의 단어 조각으로 구성할 수 있다.

3. 그 다음 단어 ban를 살펴보면,

단어 ban의 경우, [b, an], [ba, n], [ban]의 조합으로 만들 수 있는데,

  • [b, an]의 경우, b가 없으므로(-1) 불가능하므로 생략한다.
  • [ba, n]의 경우, ban으로 만들 수 있다.
    이때, ba는 이전 단계에서 1이라는 것을 이미 구했기 때문에(메모이제이션), n만 구하면 된다.
    n은 1개의 단어 조각으로 구성할 수 있으므로 n = 1이다.
  • [ban]의 경우, ban이 없으므로 불가능하므로 생략한다.

➡️ 따라서 banba(1) + n(1) = 2 개의 단어 조각으로 구성할 수 있다.

4. 그 다음 단어 bana를 살펴보면,

단어 bana는 [b, ana], [ba, na], [ban, a], [bana]의 조합으로 만들 수 있는데,

  • [b, ana]의 경우, b = -1이므로 생략
  • [ba, na]의 경우, ba = 1이고, na는 메모이제이션된 값이 없으므로 확인해야 한다. na = 1이다. ➡️ 1 + 1 = 2
  • [ban, a]의 경우, ban = 2이고, a는 메모이제이션된 값이 없으므로 확인해야 한다. a = 1이다. ➡️ 2 + 1 = 3
  • [bana]의 경우, 없으므로 -1이다.

➡️ 따라서 bana는 최솟값인 ba(1) + na(1) = 2이다.

5. 그 다음 단어 banan을 살펴보면,

단어 banan은 [bana, n]으로 가능하다.
bana(2) + n(1)이므로 banan의 단어 개수 최솟값은 3이다.

6. 마지막으로 banana를 살펴보면,

banan(3) + a(1)이므로 banana의 단어 개수 최솟값은 4이다.

이런 방식으로 작은 문제를 해결하고 점점 큰 문제를 해결해나가며 동적 계획법이 가능하다는 것을 확인할 수 있다.


JavaScript 코드

function solution(strs, t) { // t = 'banana'
  // 편의를 위해 t + 1 길이만큼의 배열을 만든다.
  const dp = Array.from(Array(t.length + 1), () => 0); // [0, 0, 0, 0, 0, 0, 0]

  // 문자열 검사를 빠르게 하기 위해 set을 사용해 문자열 리스트를 만든다.
  const strsSet = new Set(strs);
 
  for (let i = 1; i < t.length + 1; i++) { // i = 1, 2, ... 7
    dp[i] = Infinity; // 일단 최솟값을 무한으로 설정한다. 
    
    // 문자열을 자르면서 단어 조각을 찾기 위해 루프를 돈다.
    // 단어 조각은 길이가 5 이하이기 때문에 마지막까지 자를 필요 없다. (6 앞에서 자르는게 마지막임)
    for (let j = 1; j < Math.min(i + 1, 6); j++) {
      const start = i - j; // 0
      const end = i; // 1
      
      // 단어 조각이 있다면
      if (strsSet.has(t.slice(start, end))) {
        dp[i] = Math.min(dp[i], dp[i - j] + 1); // 이전 조합과 더해서 최솟값인지 체크 후 대입한다.
      }
    }
  }
  
  // 결과적으로 단어의 최솟값을 구할 수 있다. 만약 무한이라면 불가능한 조합이므로 -1을 리턴한다.
  return dp[dp.length - 1] === Infinity ? -1 : dp[dp.length - 1];
}
profile
개발 공부 기록 블로그

8개의 댓글

comment-user-thumbnail
2023년 5월 26일

고생하셨습니다~ 쉽지않네요. 알고리즘 열심히 하시는거같네요 대단합니다 ㅎ

답글 달기
comment-user-thumbnail
2023년 5월 28일

정의를 봐도 문제 풀이는 솔직히 반도 이해가 안되는 거 같습니다.. 잘보고 갑니다

+ ('b'+'a'+ +'a'+'a').toLowerCase()를 하면 'banana'가 나오는 거 아시나요? 갑자기 생각나서 적어봅니다 ㅋㅋ JS의 무서움

3개의 답글
comment-user-thumbnail
2023년 5월 28일

설명 진짜 잘 해주셔서 이해하기 좋아요 !!

답글 달기
comment-user-thumbnail
2023년 5월 28일

어우 어렵네요 ㅠㅠ 고생하셨습니다

답글 달기
comment-user-thumbnail
2023년 5월 28일

고생하셨습니다.. 진짜 열심히 하시네용 ㅠ.ㅠ

답글 달기