조합 (Combination)
- 서로 다른 n개의 수 중 순서를 생각하지 않고 r개를 선택; nCr로 표현
- 그에 반해 순열 nPr은 순서를 고려


조합의 성질
- 이항계수와 파스칼의 삼각형과 관련
- 이항계수: 이항정리에서 각 항의 계수인 조합 nCr → 쉽게 말해, 이항정리에서는 각 항의 계수가 조합으로 나타난다.
- 이항정리: 두 항의 합의 거듭제곱인 (a+b)^n을 전개한 결과를 정리한 것
- 파스칼의 삼각형: 이항계수들을 삼각형 피라미드 형태로 배열한 것


이를 토대로 코드를 짜면 다음과 같다. 아래쪽에 combi() 함수를 보면 위 성질을 적용하여 재귀 호출한 것을 알 수 있다.
import java.util.Scanner;
public class Combination {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int r = sc.nextInt();
System.out.println(combi(n, r));
}
static int combi(int n, int r) {
if (n == r || r == 0) {
return 1;
}
return combi(n - 1, r) + combi(n - 1, r - 1);
}
}
팩토리얼 정의를 이용해 조합을 나타낼 수도 있다.
import java.util.Scanner;
public class Combination_iter {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int r = sc.nextInt();
System.out.println(factorial(n) / (factorial(n - r) * factorial(r)));
}
private static int factorial(int n) {
if (n == 1) {
return 1;
}
return n * factorial(n - 1);
}
}
이처럼 자기 자신의 함수를 반복적으로 호출하면서(=재귀 호출) 코드를 실행할 수 있지만 반복문으로도 풀 수 있다. 정확히 말하면 반복문을 통해 더 효율적으로 실행할 수 있다. 왜냐하면 재귀 호출의 경우 같은 함수를 중복적으로 호출하기 때문에 메모리를 불필요하게 사용한다. 예를 들어, combi(5,3)를 호출할 경우 과정은 다음과 같다.
combi(4,3) + combi(4,2)
(combi(3,3) + combi(3,2)) + (combi(3,2) + combi(3,1))
(1 + (combi(2,2) + combi(2,1))) + ((combi(2,2) + combi(2,1)) + (combi(2,1) + combi(2,0)))
(1 + (1 + (combi(1,1) + (combi(1,0)))) + ((1 + (combi(1,1) + combi(1,0))) + ((combi(1,1)+combi(1,0)) + 1))
(1 + (1 + (1 + 1))) + ((1 + (1 + 1)) + ((1 + 1) + 1)) = 10
이렇게 보면 같은 함수가 반복적으로 호출되고 있음을 알 수 있는데, 이는 n이 커질수록 기하급수적으로 증가한다. 그렇게 되면 스택에 스택프레임이 많이 쌓이면서 언젠가는 stack overflow를 일으킬 것이다. 이에 대한 대안으로 반복문과 메모리 저장 개념을 이용해 동적 계획법을 활용할 수 있다. 동적계획법에서 메모리에 먼저 값을 저장해 두고 값을 꺼내 쓰는 메모이제이션(memoization)을 사용하는 것이다.
import java.util.Scanner;
public class Combi_loop {
static int[][] dp;
public static void main(String[] args) {
/**
* top-down 방식; 위에서 아래로
*/
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int r = sc.nextInt();
dp = new int[n + 1][r + 1];
System.out.println(combi(n, r));
}
private static int combi(int n, int r) {
if (dp[n][r] > 0) { // 값이 저장되어 있다면
return dp[n][r];
}
if (n == r || r == 0) {
return dp[n][r] = 1;
}
return dp[n][r] = combi(n - 1, r) + combi(n - 1, r - 1);
}
}
import java.util.Scanner;
public class Combi_loop {
static int[][] dp;
public static void main(String[] args) {
/**
* bottom-up 방식; 아래에서 위로
*/
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int r = sc.nextInt();
dp = new int[n + 1][r + 1];
// n == r
for (int i = 0; i <= r; i++) {
dp[i][i] = 1;
}
// r == 0
for (int i = 0; i <= n; i++) {
dp[i][0] = 1;
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= r; j++) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}
}
System.out.println(dp[n][r]);
}
}
동적 계획법(Dynamic Programming)
- 복잡한 문제를 간단한 여러 개의 문제로 나누어 푸는 방법 [참고: 위키백과]
- 가능한 모든 방법을 고려하기 때문에 시간이 조금 걸리지만 최적해를 구할 수 있다
cf. 그리디 알고리즘은 그때마다의 최적의 경로를 우선하지만 최종적인 최적해는 보장할 수 없음 - 메모이제이션(memoization)을 활용하여 중복 연산을 줄이고 수행 속도를 높인다
cf. memoization: 어떠한 정답을 구하면 그 정답을 메모해 두고 메모를 이용하는 방식 - top-down 방식과 bottom-up 방식 존재
- 최단 경로 문제(다익스트라, 벨만포드 등), LCS(최장 공통 부분 수열) 등에 적용
top-down 방식과 bottom-up 방식은 문제해결의 순서가 이름에서 드러나듯이 차이가 있는데 표로 정리해 보면 다음과 같다.
top-down 방식 | bottom-up 방식 |
1. 하향식 문제해결 방식 2. 가장 큰 문제를 방문한 후 작은 문제를 호출하여 답을 찾는다. 3. 일반적으로 재귀 함수를 이용해 구현한다. 4. 모든 부분을 다 구하지 않아도 될 때 유리하다. |
1. 상향식 문제해결 방식 2. 가장 작은 문제들로부터 답을 구해가며 전체 문제의 답을 찾는다. 3. 일반적으로 반복문을 이용해 구현한다. 4. 모든 부분을 다 구해야 할 때 유리하다. |
무작정 둘 중 어느 방식이 더 좋다고 말하는 것은 적절하지 않다. 문제 상황과 맥락을 고려하여 적절한 방식을 택하는 게 현명하다. 백준 문제를 풀 때도 재귀로 하는 게 메모리나 시간 관점에서 더 좋을 수도 있고, 반대로 반복문으로 구현하는 게 더 빠를 수도 있다. 그러니 둘 다 익혀두는 게 베스트다!
[수학 참고:https://terms.naver.com/entry.naver?docId=6605839&cid=40942&categoryId=32213,
https://yeonjewon.tistory.com/80]
이항계수
이항정리에서 각 항의 계수인 조합 nCr을 이항계수라 한다. 이항정리는 두 항의 합의 거듭제곱인 (a+b)n을 전개한 결과를 정리한 것이다. (a+b)n = nC0an + nC1an-1b + nC2an-2b² + ··· + nCn-1abn-1 + nCnbn 여기
terms.naver.com
[반복문, 재귀 참고: https://yeonjewon.tistory.com/80]
[Algorithm] 반복문(iteration) vs 재귀(recursion)
지금까지 반복적인 작업이 필요한 알고리즘 문제들(거의 필연적인..?)을 풀면서 일반 반복문(while, for)이나 재귀함수를 번갈아가며 사용했었다. 하지만 코드나 접근방식에 오류가 생겼을 때, 발
yeonjewon.tistory.com
[문제 및 알고리즘 전체 참고: https://stlab.tistory.com/194,
https://stlab.tistory.com/159#%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98]
[백준] 1010번 : 다리 놓기 - JAVA [자바]
www.acmicpc.net/problem/1010 1010번: 다리 놓기 입력의 첫 줄에는 테스트 케이스의 개수 T가 주어진다. 그 다음 줄부터 각각의 테스트케이스에 대해 강의 서쪽과 동쪽에 있는 사이트의 개수 정수 N, M (0 <
st-lab.tistory.com
[백준] 11050번 : 이항 계수 1 - JAVA [자바]
www.acmicpc.net/problem/11050 11050번: 이항 계수 1 첫째 줄에 \(N\)과 \(K\)가 주어진다. (1 ≤ \(N\) ≤ 10, 0 ≤ \(K\) ≤ \(N\)) www.acmicpc.net 문제 알고리즘 [접근 방법] 이항 계수(Binomial coefficient) 문제 자체는 매
st-lab.tistory.com
[DP 특징 참고: https://jaehoney.tistory.com/208, https://devruby7777.tistory.com/entry/Top-down-DP%EC%99%80-Bottom-up-DP%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EA%B3%BC-%EC%9E%A5%EB%8B%A8%EC%A0%90-%EC%93%B0%EB%8A%94-%EA%B2%BD%EC%9A%B0, https://velog.io/@chnh506/Top-down-%EB%B0%A9%EC%8B%9D%EA%B3%BC-Bottom-up-%EB%B0%A9%EC%8B%9D]
DP(Dynamic Programming) - Top-down과 Bottom-up
Top-down / Bottom-up DP(Dynamic Programming) 방식은 크게 두 가지로 나뉜다. Top-down 방식과 Bottom-up 방식이다. 두 방법 모두 큰 문제를 여러 개의 부분 문제들로 나누어 푸는 방식인데 아래의 차이가 있다. -
jaehoney.tistory.com
Top-down DP와 Bottom-up DP의 차이점과 장단점, 쓰는 경우
Top-down DP와 Bottom-up DPDP를 푸는 방식에는 크게 두 가지 스타일이 있다고 할 수 있습니다.첫 번째는, Top-down DP로, 큰 부분부터 작은 부분으로 쪼개지며 답을 찾습니다.두 번째는 Bottom-up DP로, 작은
devruby7777.tistory.com
Top-down 방식과 Bottom-up 방식
다이나믹 프로그래밍 문제를 푸는 두 가지 방식, Top-down과 Bottom-up
velog.io
'문제 및 이론 정리 > Algorithm' 카테고리의 다른 글
[알고리즘] 그래프 탐색 알고리즘 - DFS, BFS (5) | 2024.10.17 |
---|---|
[알고리즘] 병합 정렬(MergeSort) 핵심 아이디어 (1) | 2024.09.28 |
[알고리즘] 정렬 알고리즘 - QuickSort(퀵 정렬) (2) | 2024.09.03 |
[알고리즘] 등차수열의 합과 이진 탐색(Binary Search) (0) | 2024.08.18 |
[알고리즘] 브루트 포스(Brute Force) 알고리즘 (1) | 2024.03.22 |