💡 완전탐색으로 경우의 수를 풀 수 있는 알고리즘 중 조합에 대해 공부해보자.
조합(Combination)은 보통 모든 경우의 수를 탐색해서 해를 찾을때 사용된다. (완전탐색)
아주 아주 기본적인 이론이므로 머릿속에 넣어두자 👍🏽
서로 다른 n개의 원소 중에서 r개를 중복없이 순서에 상관 없이 선택 혹은 나열하는 것 nCr
이미지 : 조합 공식
(1, 2, 3) 의 집합에서 3(모든)개의 조합을 구하는 경우 3C3
아래의 그림처럼 요소를 선택하고 선택한 요소들을 제외한 나머지 요소가 선택될 수 있는 경우의 수들을 분개하여 나열한다.
let input = [1, 2, 3, 4];
let count = 0;
function combination(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
count++;
}
}
}
combination(input);
console.log(count);
이런 방식의 중첩 for문으로 코드를 짜면 input 요소의 개수(뽑을 개수)가 커질 수록 for 문의 depth가 .. 아주 커진다.
그래서 순열이나 조합을 구하는 알고리즘에서는 재귀 함수(Recursion)
를 이용한다.
let input = [1, 2, 3, 4];
let output = [];
let count = 0;
// arr : input 데이터 , data : output 데이터, s : 시작 인덱스, idx : 현재 위치정보, r : 선택 개수
function combinationRecursion(arr, data, s, idx, r) {
if (s === r) {
count++;
console.log(data);
return;
}
for (let i = idx; arr.length - i >= r - s; i++) {
data[s] = arr[i];
combinationRecursion(arr, data, s + 1, i + 1, r); // Recursion
}
}
combinationRecursion(input, output, 0, 0, 2); // Main Call
console.log(count);
위 예제를 보면 요소의 개수가 길어져도 for문을 추가해주지 않아도 된다.
예제 코드2를 분석해보자.
4C2
1 Loop (Main Call, i = 0)
-Call Value
arr
: [1, 2, 3, 4]data
: []s
: 0idx
: 0r
: 2-For Loop Value
i = idx
: 0arr.length - i >= r - s
: 4 >= 2 (4 - 0 >= 2 - 0)s
: 0idx
: 0data[s] = arr[i]
: data[0] = arr[0] => [1]combinationRecursion(arr, data, s + 1, i + 1, r)
: combinationRecursion(arr, [1], 1, 1, 2)1 Loop 1 Recursion(ReCall)
-Call Value
arr
: [1, 2, 3, 4]data
: [1]s
: 1idx
: 1r
: 2-For Loop Value
i = idx
: 1arr.length - i >= r - s
: 3 >= 1 (4 - 1 >= 2 - 1)s
: 1idx
: 1data[s] = arr[i]
: data[1] = arr[1] => [1, 2]combinationRecursion(arr, data, s + 1, i + 1, r)
: combinationRecursion(arr, [1, 2], 2, 2, 2)1 Loop 2 Recursion(Re:ReCall)
-Call Value
arr
: [1, 2, 3, 4]data
: [1, 2]s
: 2idx
: 2r
: 2-return
count ++
output = [1, 2]
2 Loop 1 Recursion(Return)
-For Loop Value
i
: 2arr.length - i >= r - s
: 2 >= 1 (4 - 2 >= 2 - 1)s
: 1idx
: 1data[s] = arr[i]
: data[1] = arr[2] => [1, 3]combinationRecursion(arr, data, s + 1, i + 1, r)
: combinationRecursion(arr, [1, 3], 2, 3, 2)2 Loop 2 Recursion(Re:ReCall)
-Call Value
arr
: [1, 2, 3, 4]data
: [1, 3]s
: 2idx
: 3r
: 2-return
count ++
output = [1, 3]
3 Loop 1 Recursion(Return)
-For Loop Value
i
: 3arr.length - i >= r - s
: 1 >= 1 (4 - 3 >= 2 - 1)s
: 1idx
: 1data[s] = arr[i]
: data[1] = arr[3] => [1, 4]combinationRecursion(arr, data, s + 1, i + 1, r)
: combinationRecursion(arr, [1, 4], 2, 4, 2)3 Loop 2 Recursion(Re:ReCall)
-Call Value
arr
: [1, 2, 3, 4]data
: [1, 4]s
: 2idx
: 4r
: 2-return
count ++
output = [1, 4]
4 Loop 1 Recursion(Return)
-For Loop Value
i
: 4arr.length - i >= r - s
: 0 >= 1 (4 - 4 >= 2 - 1) => for loop end, return2 Loop (Main Call, i = 1)
-For Loop Value
i
: 1arr.length - i >= r - s
: 3 >= 2 (4 - 1 >= 2 - 0)s
: 0idx
: 0data[s] = arr[i]
: data[0] = arr[1] => [2]combinationRecursion(arr, data, s + 1, i + 1, r)
: combinationRecursion(arr, [2], 1, 2, 2)2 Loop 1 Recursion(ReCall)
-Call Value
arr
: [1, 2, 3, 4]data
: [2]s
: 1idx
: 2r
: 2-For Loop Value
i = idx
: 2arr.length - i >= r - s
: 2 >= 1 (4 - 2 >= 2 - 1)s
: 1idx
: 2data[s] = arr[i]
: data[1] = arr[2] => [2, 3]combinationRecursion(arr, data, s + 1, i + 1, r)
: combinationRecursion(arr, [2, 3], 2, 3, 2)2 Loop 2 Recursion(Re:ReCall)
-Call Value
arr
: [1, 2, 3, 4]data
: [2, 3]s
: 2idx
: 3r
: 2-return
count ++
output = [2, 3]
...
위와 같이 해를 찾는 도중 해가 아니어서 막히면, 되돌아가서 다시 해를 찾아가는 기법을
백트래킹
이라고한다.
재귀 함수는 빈번히 활용되므로 내부 로직의 이해를 잘 해야한다 💡
45C6 일 때,
45! / ( 6! * (45-6)! )
위와 같이 계산하면 머리 아프니까!
더 간단하고, 힘이 적게 들게 계산
45 / 1 × 44 / 2 × 43 / 3 × 42 / 4 × 41 / 5 × 40 / 6 = 8145060
45 / 1 은 쓸데없는 짓이지만, Factorial 이 원래 이론상 쓸데없이 1도 곱하는 거라서.
더 간단히,
45 × 44 / 2 × 43 / 3 × 42 / 4 × 41 / 5 × 40 / 6 = 8145060