[프로그래머스] LV.3 최고의 집합 (JS)

KG·2021년 4월 17일
4

알고리즘

목록 보기
21/61
post-thumbnail

문제

자연수 n 개로 이루어진 중복 집합(multi set, 편의상 이후에는 "집합"으로 통칭) 중에 다음 두 조건을 만족하는 집합을 최고의 집합이라고 합니다.

  1. 각 원소의 합이 S가 되는 수의 집합
  2. 위 조건을 만족하면서 각 원소의 곱 이 최대가 되는 집합

예를 들어서 자연수 2개로 이루어진 집합 중 합이 9가 되는 집합은 다음과 같이 4개가 있습니다.

{ 1, 8 }, { 2, 7 }, { 3, 6 }, { 4, 5 }

그중 각 원소의 곱이 최대인 { 4, 5 }가 최고의 집합입니다.

집합의 원소의 개수 n과 모든 원소들의 합 s가 매개변수로 주어질 때, 최고의 집합을 return 하는 solution 함수를 완성해주세요.

제한

  • 최고의 집합은 오름차순으로 정렬된 1차원 배열(list, vector) 로 return 해주세요.
  • 만약 최고의 집합이 존재하지 않는 경우에 크기가 1인 1차원 배열(list, vector) 에 -1 을 채워서 return 해주세요.
  • 자연수의 개수 n은 1 이상 10,000 이하의 자연수입니다.
  • 모든 원소들의 합 s는 1 이상, 100,000,000 이하의 자연수입니다.

입출력 예시

nsresult
29[4, 5]
21[-1]
28[4, 4]

풀이

간단하게 생각하면 매우 간단하게 풀 수 있고 복잡하게 생각하면 한없이 빙빙 돌아가서 풀 수도 있는 문제다. 여기서는 최대한 간단하게 수학적으로 접근을 해보자.

먼저 문제에서 주어진 조건은 중복집합(Multiset)이기 때문에 같은 원소의 중복을 허용한다. 입출력 예시의 3번째 항목과 같이 {4, 4} 와 같은 형식이 가능하다는 것이다. 이를 유의하며 문제를 풀어보자.

먼저 주어진 자연수 sn개의 원소들의 합으로 구성된다. 이때 n개의 원소들의 곱이 최댓값이 되기 위해서는 가장 큰 원소값을 기준으로 응집되어 있어야 한다. 즉 자연수기 때문에 n개의 원소값이 모두 동일하거나 편차가 가장 적게끔 분포되어 있어야 이들의 곱이 가장 큰 수가 될 수 있다. 여기서 가장 큰 원소값이 될 수 있는 녀석을 시작 최댓값이라고 명명해보고, 아래 예시로 직접 살펴보자.

  1. { 1, 8 }, { 2, 7 }, { 3, 6 }, { 4, 5 }

    • 부분집합 중에 편차가 가장 고른 것은 { 4, 5 } 이다.
    • 또한 n = 2 일때 4가 가장 큰 원소값, 즉 시작 최댓값이 된다 ({4, 5} 와 {5, 4} 는 동일하므로 그 이후는 체크 X)
  2. { 1, 7 }, { 2, 6 }, { 3, 5 }, { 4, 4 }

    • 부분집합 중에 편차가 가장 고른 것은 { 4, 4 } 이다.
    • 또한 n = 2 일때 4가 가장 큰 원소값, 즉 시작 최댓값이 된다 ({3, 5} 와 {5, 3} 은 동일하므로 그 이후는 체크 X)

그렇다면 우리는 가장 큰 원소값이 될 수 있는 시작 최댓값을 찾아야 한다. 눈치 챘을수도 있겠지만 이는 아주 간단하게 구해줄 수 있는데 바로 s / n 으로 구할 수 있다. 대단한 알고리즘은 아니고 간단한 수학적 지식인데, n개를 가지고 s 라는 자연수를 만들 수 있다면 sn 으로 나눈 그 시작 최대값이 된다. n 개를 모두 사용하여 s를 만들어야 하기 때문에 시작 최댓값은 당연히 보다 작아질 수 없다.

// 몫이 정수로 나누어 떨어지지 않을 수 있다.
// 따라서 소수점 아래는 버림한다.
const share = s / n >> 0;

// share가 0이 되는 경우는 n개의 자연수로
// s 라는 값을 만들 수 없는 경우이다.
// 따라서 [-1]을 리턴하도록 한다.
if(!share) return [-1];

이때 나누어 떨어지지 않는 경우가 생길 것 이다. 위 입출력 예제 n = 2 이고 s = 9가 그러하다. 나누어 떨어지지 않는다는 것은 그 나머지 만큼을 구해진 n개의 에 다시 배분할 수 있다는 것을 의미한다. 앞서서 우리는 편차가 고르게 분포되어 있어야 가장 최댓값이 완성될 수 있다는 것을 보았기 때문에 뒤에서부터 나머지의 값이 0이 될 때 까지 1씩 더해주면 될 것 이다. 이를 코드로 살펴보자.

// 나누어 떨어지지 않는 경우가 있을 수 있다.
// 이때의 나머지 값을 구해준다.
const rest = s % n;
// 정답은 n개의 자연수로 구성된다.
// 이때 시작최댓값은 몫이 었으므로
// n개의 share 값을 가진 배열을 만들어준다.
// 만약 rest = 0, 즉 나누어 떨어진 경우라면
// 중복된 원소들의 집합 자체가 정답이 될 것이다.
const answer = new Array(n).fill(share);

// 남은 rest 값 만큼 반복을 돌면서
// 배열의 마지막 원소부터 1을 더해준다.
// 또는 처음부터 1을 더하고 오름차순 정렬을 해도 상관없다. (아래 경우보다 조금 더 오랜 시간이 소요되겠지만 통과엔 문제없다)
for(let i = 0; i < rest; i++) 
  answer[answer.length - 1 - i]++;

주저리 주저리 말이 길어졌지만 (항상 간단한 것도 길게 설명하는게 특징이다) 전체 코드로 보면 이해하는데 크게 어렵지 않을 거라 생각한다. 다음은 주석을 제외한 전체 코드이다.


코드

function solution(n, s) {
  const share = s / n >> 0;
  
  if(!share) return [-1];
  
  const rest = s % n;
  const answer = new Array(n).fill(share);
  
  for(let i = 0; i < rest; i ++)
    answer[answer.length - 1 - i]++;
  
  return answer;
}

출처

https://programmers.co.kr/learn/courses/30/lessons/12938

profile
개발잘하고싶다

1개의 댓글

comment-user-thumbnail
2022년 9월 16일

혹시 "원소들의 곱이 최댓값이 되기 위해서는 가장 큰 원소값을 기준으로 응집되어 있어야 한다" 의 좀 더 수학적으로 구체적인 해설이 가능할까요?
저도 몇 번 테스트 하다가 대충 그러겠거니 하고 풀긴 했는데... 왜 항상 그런건지 증명은 못하겠네요ㅎㅎ

답글 달기