조합 알고리즘
개요
- 조합 알고리즘에 대해 알아보기 전에, 먼저 조합에 대해 알아보자.
조합이란?
- n개의 숫자 중, r개의 숫자를 순서없이 뽑는 것
공식
- ‘하나의 원소를 선택할 경우’ + ‘하나의 원소를 선택하지 않을 경우’의 합이다.
- 즉, n개 중 r개를 뽑는 경우의 수는 ‘하나의 원소를 선택하고 나머지를 뽑는 경우’와 ‘하나의 원소를 제외하고 뽑는 경우’로 이루어진다.
자세한 것은 아래 예시를 통해 설명한다.
예시
주어진 배열
{1, 2, 3}
뽑는 개수 (r)
- 2개
가능한 조합
{1, 2}
,{1, 3}
,{2, 3}
공식 적용
- 인 이유?
- 3개 중 하나는 뽑아두고(
n-1
), 남은 횟수(r-1
)만큼 마저 뽑는다. - 예시
- 1을 뽑아둔다. → 남은 원소 = 3-1 = 2 = n-1
- 남은 횟수만큼 뽑는다. → 남은 횟수 = 2-1 = 1
- 3개 중 하나는 뽑아두고(
- 인 이유?
- 3개 중 하나를 제외하고(
n-1
), 뽑는다. - 예시
- 1을 제외한다. → 남은 원소 = 3-1 = 2 = n-1
- 뽑는다. → 2 = r
- 3개 중 하나를 제외하고(
- 따라서 해당 공식을 이 나올때까지 반복하면, 을 구할 수 있다.
조합 알고리즘
조합의 경우의 수 구하기
- 조합의 실제 경우를 구하는 것이 아닌, 단순히 경우의 수 만 구하는 알고리즘에 대해 먼저 알아보자.
코드
public class 조합_경우의_수 {
public int getCombinationCaseNum(int n, int r) {
/*
* n == r 이라면, 모두 뽑는 경우 하나만 존재한다.
* r == 0 이라면, 모두 뽑지 않는 경우 경우 하나만 존재한다.
*/
if (n == r || r == 0) {
return 1;
}
return getCombinationCaseNum(n-1, r-1) + getCombinationCaseNum(n-1, r);
}
}
- 코드가 상당히 단순하다.
- 단순히 총 경우의 개수만 구한다면, 공식을 코드화하면 된다.
실제 조합 구하기
- 이번에는 실제 조합을 구해보자.
- 배열의 처음부터 마지막까지 돌며 아래 케이스를 모두 고려해야 한다.
- 현재 인덱스의 원소를 선택하는 경우 ()
- 현재 인덱스의 원소를 선택하지 않는 경우 ()
- 재귀를 통한 완전탐색 방식으로 실제 조합을 구할 수 있다.
- 백트래킹 등의 방식도 있지만, 생략한다.
- 코드를 통해 설명하겠다.
코드
코드는 한번 훑어본 뒤, 아래 설명을 참고하자. 그리고 다시 코드를 보자.
public class 조합_구하기 {
public static void getCombination(int[] numAry, boolean[] visited, int depth, int n, int r) {
if (r == 0) { //뽑아야하는 만큼 뽑았으므로, 출력 후 종료
print(numAry, visited);
return;
}
if (depth == n) { //모든 원소를 둘러보았으므로, 종료
return;
}
visited[depth] = true; //현재 원소를 뽑았을 때
getCombination(numAry, visited, depth+1, n, r-1); // n-1Cr-1
visited[depth] = false; //현재 원소를 뽑지 않았을 때
getCombination(numAry, visited, depth+1, n, r); // n-1Cr
}
//뽑은 원소를 출력하는 메서드
private static void print(int[] numAry, boolean[] visited) {
System.out.print("{");
for (int i = 0; i < visited.length; i++) {
if (visited[i]) {
System.out.print(numAry[i] + " ");
}
}
System.out.print("}\n");
}
}
- 변수
depth
- 현재 인덱스를 가르킨다.
- 순열이 아닌 조합이기 때문에, 이전에 살펴본 원소는 더이상 고려대상이 아니다.
따라서 변수depth
는 무조건 1씩 증가한다.- 즉 이전에 뽑을지 말지 판단했던 원소는 다음 뽑기에서 제외된다. (뽑지 않더라도)
- 배열
boolean[] visited
visited[i]
= i번째 원소를 뽑을지, 말지에 대한 정보- 현재 인덱스(
depth
)에 위치한 원소를 뽑는다면 →visited[depth] = true
- 현재 인덱스(
depth
)에 위치한 원소를 뽑지 않는다면 →visited[depth] = false
- 매개변수
n
- 조합 공식에선
n-1
을 사용한다. 하지만 위 코드에선 매개변수n
은 변화하지 않는다. - 왜냐하면, 변수
depth
가 1씩 증가하므로,n
이 1씩 감소될 필요가 없기 때문이다.
- 조합 공식에선
- 재귀 종료 조건
depth == n
- 모든 원소를 살펴보았으므로 종료한다.
- 단
depth == n
이더라도, 뽑아야하는 횟수만큼 뽑았다는 보장이 없기 때문에 뽑은 원소를 출력하지는 않는다.
r == 0
- 뽑아야하는 횟수만큼 뽑았으므로 종료한다.
- 뽑아야하는 횟수만큼 뽑았기 때문에, 뽑은 원소를 출력한다.
- 이때
visited
배열에서true
인 index가 ‘뽑은 원소의 index’이다.
- 이때
재귀의 흐름
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
이하 생략…
중복 가능한 조합 구하기
코드
public class 중복_조합 {
static int[] numbers = new int[] {1, 2, 3};
static int r = 3;
static int[] result = new int[3];
//3개의 원소 중, 순서를 고려하지 않고 중복해서 3개를 뽑기
public static void combination(int depth, int r, int start) { //n은 필요없다.
if (r == 0) { //3개를 모두 뽑았다면
printResult();
return;
}
for (int i = start; i < numbers.length; i++) {
result[depth] = numbers[i];
combination(depth + 1, r - 1, i);
}
}
private static void printResult() {
for (int number : result) {
System.out.print(number + " ");
}
System.out.println();
}
}
상세 설명
visited
배열이 필요없다.- 중복이 가능하기 때문이다.
- 위와는 다르게
for
문을 사용하였다.