n개의 값 중에서 r 개의 숫자를 순서를 고려하지 않고 나열한 경우의 수
순열과는 순서를 고려하지 않는 다는 점이 다르다
순서가 관계 없다는 건 ?
[1,2] 와 [2,1] 이 있을 때 동일하게 여긴다는 의미
계산식으로는 nCr 이라고 표현한다.
nCr
= nPr / r!
= n! / ((n-r)! * r!)
예시로 서로 다른 3개의 숫자(1,2,3) 중 중복되지 않으면서 순서와 관계 없이 2개를 뽑는다고 가정해보자
3C2
= 3! / 1! * 2!
= 3! / 2!
= (3x2x1) / (2x1)
= 3
로 총 3개의 경우의 수가 나오게 된다 (1,2 / 1,3 / 2,3)
조합에 대해서 본격적으로 들어가기 전에 점화식에 대해서 한 번 짚고 넘어가자.
어떤 함수를 자신보다 더 작은 변수에 대한 함수와의 관계로 표현한 것 이다.
예를 들어 피보나치 수열을 재귀 함수로 구현할 때 아래와 같은 코드가 만들어 지는데
function fibonacci(n) {
if ( n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
base case 가 되는 if 의 내용을 제외한 fibonacci(n - 1) + fibonacci(n - 2) 가 바로 피보나치 수열에 대한 점화식이 된다.
더 자세한 내용은 여기 블로그 참고 ❗️ 완전 정리 잘 되어 있다 !! ➡️ 점화식 (TIL 42일차)
조합의 점화식은 다음과 같다.
n-1Cr-1 + n-1Cr
➡️ 두 가지의 경우를 합쳐야 n개 중 하나를 뽑는 경우의 수가 나오게 된다
이렇게 글로만 봤을 땐 이해가 잘 안될 수도 있기 때문에 예시를 통해서 더 자세히 알아보자
예시)
{1,2,3} 에서 3C2 조합
1뽑기
- 2뽑기 ➡️ 🌟 {1,2} 조합 완성 종료
- 2안뽑기
- 3뽑기 ➡️ 🌟 {1,3} 조합 완성 종료
- 3안뽑기 ➡️ 끝까지옴 종료
1안뽑기
- 2뽑기
- 3뽑기 ➡️ 🌟 {2,3} 조합 완성 종료
- 3안뽑기 ➡️ 끝까지 옴 종료
- 2안뽑기
- 3뽑기 ➡️ 끝까지 옴 종료
- 3안뽑기 ➡️ 끝까지 옴 종료
조합과 순열은 상당히 유사한데 차이점이라고 하면
조합은 순서와 상관없이 나열 하는 경우의 수
순열은 순서를 고려해 나열 하는 경우의 수 이다
예시를 들어보자면 [1,2,3] 이라는 배열이 있을 때 2개를 뽑는 경우의 수는
순열인 경우 경우의 수가 6가지
[1,2] / [1,3] / [2,1] / [2,3] / [3,1] / [3,2]
조합은 순서를 상관하지 않기 때문에 경우의 수가 3가지
[1,2] / [2,3] / [3,1]
가 나오게 된다
순열 관련된 포스팅은 [알고리즘] 순열 (Permutation) 를 참고
위에서 사용한 점화식을 기반으로 재귀함수를 구현한다
#include <iostream>
#include <vector>
using namespace std;
int R;
vector<int> cout_arr(9);
/**
arr = 대상 배열
comb = 출력 배열
r = 남은 뽑아야 할 갯수
index = comb 의 인덱스
depth = 대상 배열에서 뽑을 원소를 결정하는 인덱스
*/
void Combination(vector<int> arr, vector<int> comb, int r, int index, int depth) {// depth == 점화식의 n역할
if (r == 0) { // 뽑아야할 갯수가 모두 찬 경우에는 comb 에 들어있는 값 모두 출력
for(int i = 0; i < R; i++) cout << comb[i] << " ";
cout << endl;
} else if (depth == arr.size()) { // 뽑을 원소 인덱스가 대상 배열의 마지막까지 온 경우 return
return;
} else {
// arr[depth] 를 뽑은 경우
comb[index] = arr[depth];
// cout << "Combination1 comb["<<comb[0]<<","<<comb[1]<<","<<comb[2]<< "] / r="<<r-1<<", index="<<index+1<<", depth="<<depth+1<<endl;
Combination(arr, comb, r - 1, index + 1, depth + 1); // n-1Cr-1: 특정 원소를 뽑은 경우
// cout << "Combination2 comb["<<comb[0]<<","<<comb[1]<<","<<comb[2]<< "] / r="<<r<<", index="<<index<<", depth="<<depth+1<<endl;
// arr[depth] 를 뽑지 않은 경우
Combination(arr, comb, r, index, depth + 1); // n-1Cr: 특정 원소를 뽑지 않은 경우
}
}
int main() {
vector<int> arr = {1, 2, 3, 4, 5}; // n = 5
cout << "[1,2,3,4,5] 의 배열에서 몇개를 뽑을지 입력해주세요 : ";
cin >> R;
Combination(arr, cout_arr, R, 0, 0); // 5Cr
return 0;
}
백트래킹이란?
현재 상태에서 가능한 모든 후보군을 따라 들어가며 해결책에 대한 후보를 구축해 나아가다 가능성이 없다가 판단되면 즉시 후보를 포기하면서 정답을 찾아가는 범용적인 알고리즘
EX) DFS (깊이 우선 탐색)
#include <iostream>
#include <vector>
#define MAX 5 // 1부터 5까지 수의 순열을 구한다.
using namespace std;
int visit[MAX]; // 방문 여부를 저장하는 배열
void combination_DFS(int index, int count) { // count개의 수를 이용해 조합을 만듬
if (count == 3) {
for (int i = 1; i <= MAX; i++) {
if (visit[i] == true) // 조합과 순열의 차이 (조합은 중복 제거)
cout << i << " ";
}
cout << endl;
return;
}
for (int i = index; i <= MAX; i++) {
// 이미 방문한 곳이라면 건너뛴다.
if (visit[i] == true) continue;
visit[i] = true; // 방문 표시를 남긴다.
combination_DFS(i, count + 1);
visit[i] = false; // 체크 취소 (다른 자식 노드를 체크하기 위해)
}
}
int main() {
combination_DFS(1, 0);
return 0;
}
이름 | 설명 |
---|---|
next_permutation | 현재 나와 있는 수열에서 인자로 넘어간 범위에 해당하는 다음 순열을 구하고 true를 반환한다. 다음 순열이 없다면(다음에 나온 순열이 순서상 이전 순열보다 작다면) false를 반환. |
prev_permutation | 현재 나와 있는 수열에서 인자로 넘어간 범위에 해당하는 이전 순열을 구하고 true를 반환한다. 이전 순열이 없다면(다음에 나온 순열이 순서상 이전 순열보다 크다면) false를 반환. |
prev_permutation
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
const int r = 2;
vector<char> arr{'a', 'b', 'c', 'd'};
vector<bool> temp(arr.size(), false);
// 앞부터 r개의 true가 채워진다. 나머지 뒤는 false.
// {true, true, false, false}
for(int i = 0; i < r; i ++)
temp[i] = true;
do {
for (int i = 0; i < arr.size(); ++i) {
if (temp[i] == true)
cout << arr[i] << ' ';
}
cout << endl;
} while (prev_permutation(temp.begin(), temp.end()));
}
모든 조합을 구하려면 bool 배열의 초기 상태가 내림차순 정렬이여야 한다.
: true 가 false 보다 앞에 와야 한다.
next_permutation
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
const int r = 2;
vector<char> arr{'a', 'b', 'c', 'd'};
vector<bool> temp(arr.size(), true);
// 뒤에 false가 n-r개 채워지고 뒤에 true 가 r개 채워진다.
// {false, false, true, true}
for(int i = 0; i < arr.size() - r; i ++)
temp[i] = false;
do {
for (int i = 0; i < arr.size(); ++i) {
if(temp[i] == true)
cout << arr[i] << " ";
}
cout << endl;
} while (next_permutation(temp.begin(), temp.end()));
}
모든 조합을 구하려면 bool 배열의 초기 상태가 오름차순 정렬이여야 한다.
: false 가 true 보다 앞에 와야 한다.
재귀호출 시 2번의 함수를 호출하기 때문에 O(2n)
2n = (1+1)n = nC0 + nC1+nC2+...+nCn
(C++) 조합(Combination) 구현하기
조합 알고리즘
점화식 (TIL 42일차)
[Algorithm] C++에서 next_permutation 함수(혹은 prev_permutation 함수)를 통해서 순열 구하기