문제 소개
JavaScript에서 하나의 숫자를 여러 개의 난수로 나눠야 하는 경우가 종종 있습니다. 예를 들어 총 포인트를 사용자들에게 무작위로 분배하거나, 게임에서 아이템 수량을 나누는 상황 등이 대표적입니다.
이번 글에서는 다음 조건을 만족하는 JavaScript 함수를 작성해 보겠습니다.
- 첫 번째 인자로 전체 합계가 될 숫자를 받습니다.
- 두 번째 인자로 각 숫자가 넘지 말아야 할 최댓값을 받습니다.
- 생성된 네 개의 난수를 모두 더하면 첫 번째 인자와 같아야 합니다.
- 네 개의 숫자 중 어느 하나도 두 번째 인자(최댓값)를 초과해서는 안 됩니다.
예를 들어 함수에 다음과 같은 인자가 주어졌다고 가정해 봅시다 −
const n = 10; const max = 4;
그렇다면 아래와 같은 결과도 유효한 조합입니다 −
const output = [3, 2, 3, 2];
단, 숫자의 중복은 허용됩니다.
구현 코드
이 문제를 해결하는 코드는 다음과 같습니다 −
const total = 10;
const max = 4;
const fillWithRandom = (max, total, len = 4) => {
let arr = new Array(len);
let sum = 0;
do {
for (let i = 0; i < len; i++) {
arr[i] = Math.random();
}
sum = arr.reduce((acc, val) => acc + val, 0);
const scale = (total - len) / sum;
arr = arr.map(val => Math.min(max, Math.round(val * scale) + 1));
sum = arr.reduce((acc, val) => acc + val, 0);
} while (sum - total);
return arr;
};
console.log(fillWithRandom(max, total));실행 결과
콘솔에 출력되는 결과는 다음과 같습니다 −
[ 3, 3, 2, 2 ]
난수를 기반으로 하기 때문에 실행할 때마다 출력 결과는 달라집니다.
코드 동작 원리
이 알고리즘이 어떻게 작동하는지 단계별로 살펴보겠습니다.
- 난수 생성: 길이가 4인 배열을 만들고 각 요소에
Math.random()으로 0과 1 사이의 실수 난수를 채웁니다. - 비율 계산: 배열의 합계를 구한 뒤, 목표 총합(total)에서 요소 개수(len)를 뺀 값을 합계로 나눠 스케일 계수를 계산합니다.
- 정수 변환 및 최댓값 제한: 각 난수에 스케일을 곱하고 1을 더한 후 반올림해 정수로 만듭니다.
Math.min()을 사용해 값이 최댓값을 넘지 않도록 제한합니다. - 검증 반복: 결과 배열의 합이 목표 총합과 일치하지 않으면
do...while반복문에 의해 처음부터 과정을 다시 수행합니다.
이처럼 재시도 방식(rejection sampling)을 활용하면 조건을 만족하는 조합을 확실하게 얻을 수 있습니다. 다만 목표 총합과 최댓값 설정에 따라 반복 횟수가 늘어날 수 있다는 점은 참고하세요.