자연수 n 개로 이루어진 중복 집합(multi set, 편의상 이후에는 "집합"으로 통칭) 중에 다음 두 조건을 만족하는 집합을 최고의 집합이라고 합니다.
예를 들어서 자연수 2개로 이루어진 집합 중 합이 9가 되는 집합은 다음과 같이 4개가 있습니다.
{ 1, 8 }, { 2, 7 }, { 3, 6 }, { 4, 5 }
그중 각 원소의 곱이 최대인 { 4, 5 }가 최고의 집합입니다.
집합의 원소의 개수 n과 모든 원소들의 합 s가 매개변수로 주어질 때, 최고의 집합을 return 하는 solution 함수를 완성해주세요.
n | s | result |
---|---|---|
2 | 9 | [4, 5] |
2 | 1 | [-1] |
2 | 8 | [4, 4] |
간단하게 생각하면 매우 간단하게 풀 수 있고 복잡하게 생각하면 한없이 빙빙 돌아가서 풀 수도 있는 문제다. 여기서는 최대한 간단하게 수학적으로 접근을 해보자.
먼저 문제에서 주어진 조건은 중복집합(Multiset)이기 때문에 같은 원소의 중복을 허용한다. 입출력 예시의 3번째 항목과 같이 {4, 4} 와 같은 형식이 가능하다는 것이다. 이를 유의하며 문제를 풀어보자.
먼저 주어진 자연수 s
는 n
개의 원소들의 합으로 구성된다. 이때 n
개의 원소들의 곱이 최댓값이 되기 위해서는 가장 큰 원소값을 기준으로 응집되어 있어야 한다. 즉 자연수기 때문에 n
개의 원소값이 모두 동일하거나 편차가 가장 적게끔 분포되어 있어야 이들의 곱이 가장 큰 수가 될 수 있다. 여기서 가장 큰 원소값이 될 수 있는 녀석을 시작 최댓값
이라고 명명해보고, 아래 예시로 직접 살펴보자.
{ 1, 8 }, { 2, 7 }, { 3, 6 }, { 4, 5 }
시작 최댓값
이 된다 ({4, 5} 와 {5, 4} 는 동일하므로 그 이후는 체크 X){ 1, 7 }, { 2, 6 }, { 3, 5 }, { 4, 4 }
시작 최댓값
이 된다 ({3, 5} 와 {5, 3} 은 동일하므로 그 이후는 체크 X)그렇다면 우리는 가장 큰 원소값이 될 수 있는 시작 최댓값
을 찾아야 한다. 눈치 챘을수도 있겠지만 이는 아주 간단하게 구해줄 수 있는데 바로 s / n
으로 구할 수 있다. 대단한 알고리즘은 아니고 간단한 수학적 지식인데, n
개를 가지고 s
라는 자연수를 만들 수 있다면 s
를 n
으로 나눈 그 몫이 시작 최대값
이 된다. 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;
}
혹시 "원소들의 곱이 최댓값이 되기 위해서는 가장 큰 원소값을 기준으로 응집되어 있어야 한다" 의 좀 더 수학적으로 구체적인 해설이 가능할까요?
저도 몇 번 테스트 하다가 대충 그러겠거니 하고 풀긴 했는데... 왜 항상 그런건지 증명은 못하겠네요ㅎㅎ