이항 계수
한 문장으로 말하자면, 두 개의 항(이항)을 전개하여 계수로 나타낸 것
즉, (a+b)n 을 전개하였을 때 계수를 나타내는 것이다.
사실 필자는 여기까지는 이해를 못했다가, (kn)가 조합 nCk를 의미한다는 것을 보고 감이 잡혔다.
위 (a+b)n에서
n = 2인 경우는 a2+2ab+b2이고, 계수는 [1, 2, 1]이다.
n = 3 인 경우는 a3+3a2b+3ab2+b3 이고, 계수는 [1, 3, 3, 1]이다.
이러한 이항 계수는 n의 제곱에 대해 전개하면 다음과 같은 항이 나타난다.
(a+b)n=[anb0,an−1b1,an−2b2,...,an−rbr,...,a2bn−2,a1bn−1,a0bn]
위 내용은 계수는 제외하고 항만 나타낸 것이다.
각 항에서 계수를 구하고 싶다. 즉, (a+b)n을 전개하였을 때, an−rbr에 대한 계수를 구하는 것이 목표이다.
그렇다면 계수는 어떻게 구해야 할까?
먼저 (a+b)3을 봐보자.
(a+b)3=(a+b)(a+b)(a+b)이다. 이때 각 b를 b1, b2, b3 으로 생각해보자.
그렇다면
(a+b)(a+b)(a+b) = (a+b1)(a+b2)(a+b3)이다.
(a+b1)(a+b2)(a+b3) = a3+(b1+b2+b3)a2+(b1b2+b2b3+b3b1)a+b1b2b3 이 된다.
위 식을 해석해보면
a3 항은 [b1,b2,b3] 중에서 하나도 뽑지 않았다. 즉 0개를 뽑는다.
a2 항은 [b1,b2,b3] 중에서 하나씩 뽑아서 모두 더한 값이다.
a1 항은 [b1,b2,b3] 중에서 두개씩 뽑아서 곱한 후 모두 더한 값이다.
a0 항은 [b1,b2,b3] 중에서 세개를 뽑아 곱한 값이다.
쉽게 설명하자면 b가 n개 있을 때, n개중 r개를 뽑는다는 말이다.
n개중 r개를 뽑는다? -> 바로 nCr이 생각났다!
즉,
a3 항은 [b1,b2,b3] 중에서 하나도 뽑지 않았으니 3C0이 된다.
a2 항은 [b1,b2,b3] 중에서 하나씩 뽑는 것이니 3C1이 된다.
a1 항은 [b1,b2,b3] 중에서 두개씩 뽑는 것이니 3C2이 된다.
a0 항은 [b1,b2,b3] 중에서 세개를 뽑는 것이니 3C3이 된다.
그렇다면 (a+b)n에서는?
(a+b)n=nC0anb0+nC1an−1b1+nC2an−2b2+...+nCran−rbr+...+nCn−1a1bn−1+nCna0bn 이 된다.
즉, (a+b)n=r=0∑nnCran−rbr 이다.
아무튼! 결론적으로 이항계수 (kn)를 구해달라는 뜻은 nCk를 구하여라 라는 뜻과 같다!
그렇다면 nCk은 어떻게 구하나?
고등학교 시간때 배운걸 기억해보자.
nCr=r!(n−r)!n! 이다.
즉, n!과 r!, (n−r)! 을 구해서 계산하면 되겠다.
하지만, 적은수에서는 가능하지만 크기가 커진다면? 엄청난 메모리를 소모할 수 있겠다.
따라서 다른 방식으로 문제를 푸는 것이 바람직하다.
먼저 조합에 대한 성질에 대해 알아보자
- n+1Cr+1=nCr+nCr+1 이다.
해당 성질에 대해 알고 있었다고하면 필자는 까먹었다...
따라서 풀어 써보자면,
n+1Cr+1=(r+1)!(n+1−(r+1))!(n+1)!=(r+1)!(n−r)!(n+1)! 이다.
nCr+nCr+1=r!(n−r)!n!+(r+1)!(n−(r+1))!n!=(r+1)!(n−r))!n!(r+1)+(r+1)!(n−r))!n!(n−r)=
(r+1)!(n−r))!n!((r+1)+(n−r))=(r+1)!(n−r))!n!(n+1)=(r+1)!(n−r))!(n+1)! 이다.
즉 n+1Cr+1=nCr+nCr+1이다!!!
그렇다면, nCr=n−1Cr−1+n−1Cr일 것이다.
위 점화식을 파스칼의 삼각형이라 부른다.
- nC0=nCn=1이다.
해당 내용은 알고 있다.
위 두가지를 사용하면 재귀 알고리즘을 통하여 구현이 가능하다
int pascalRule(int N, int K){
if(N == K || N == 0){
return 1;
}
return pascalRule(N - 1, K - 1) + pascalRule(N - 1, K);
}
하지만, 위 코드처럼 구현하여 재귀 알고리즘을 사용할 경우, 이미 구한 값이더라도 한번 더 구해야하는, 즉 중복 문제가 발생한다. 결국 메모이제이션을 하지 않으면 성능은 많이 떨어지게 된다.
따라서 메모이제이션을 해주어 더욱 효율이 좋은 알고리즘으로 구현 할 수 있다.
int[][] dp = new int[N + 1][K + 1];
int pascalRule(int N, int K){
if(dp[N][K] > 0){
reutnr dp[N][K];
}
if(N == K || K == 0){
return dp[N][K] = 1;
}
return dp[N][K} = pascalRule(N - 1, K - 1) + pascalRule(N - 1, K);
}
필자는 가장 효율적인 아래 알고리즘으로 BOJ 11050 이항 계수 1을 풀었다.
해당 알고리즘 풀이는 이항 계수 1 여기에 있다.
응용 문제(BOJ 11051 이항 계수 2)
해당 문제에서는 위에서 설명한 이항 계수 내용에 추가적으로 구한 값을 10007로 나눈 나머지값을 구하여야 한다.
하지만 N의 범위가 1≤N≤1000 이다.
기본적으로 long형은 최대 263−1, 약 92경?!?! 이다.
하지만 20!의 경우 2,432,902,008,176,640,000 = 약 243경이다....
20만 넘어가도 계산 long 범위를 넘어가므로, 계산할수가 없어진다.
그렇다면 어떻게 해야하는가?
이때 필요한 성질이 모듈러 연산이다.
모듈러 연산이란,
(a+b)modm=((amodm)+(bmodm))modm
(a×b)modm=((amodm)×(bmodm))modm
을 만족한다.
이해가 안간다면, 아래 성질 유도 과정을 봐보자.
먼저 amodm=r1,bmodm=r2 라고 가정하자.
그렇다면, a=mq1+r1 이고, b=mq2+r2 이고,
a+b=m(q1+q2)+r1+r2 이다.
여기서, r1+r2=m∗q3+r3 이라고 가정해보자.
그렇다면 a+b=m(q1+q2+q3)+r3 이다.
즉, (a+b)modm=r3 이므로,
(a+b)modm=(r1+r2)modm이고,
(a+b)modm=((amodm)+(bmodm))modm 이다.
곱셈 연산 유도는 다음과 같다.
먼저 amodm=r1,bmodm=r2 라고 가정하자.
그렇다면, a=mq1+r1 이고, b=mq2+r2 이고,
a×b=(mq1+r1)×(mq2+r2) 이다.
전개하면 m2q1q2+mq1r1+mq2r1+r1r2 가 되고,
m의 배수는 mod m 에서 모두 0이 되므로
(a×b)modm=(r1×r2)modm 이다.
따라서 (a×b)modm=((amodm)×(bmodm))modm 이다.
위 모듈러 연산 내용을 코드로 바꿔보면,
(a + b) % m = ((a % m) + (b % m)) % m 이고,
(a b) % m = ((a % m) (b % m)) % m 이다.
우리는 nCr=n−1Cr−1+n−1Cr 해당 값을 메서드로 구현해 사용했었다.
여기에서 nCrmodp=(n−1Cr−1+n−1Cr)modp 이다.
따라서, (n−1Cr−1+n−1Cr)modp=(n−1Cr−1modp+n−1Crmodp)modp 이다.
해당 식을 추가해주면 되겠다.
해당 내용의 풀이는 이항 계수 2 를 참고하자.
응용 문제(BOJ 11401 이항 계수 3)
해당 문제는 N의 크기가 무려 4000000 이고, 1,000,000,007로 나눈 나머지를 출력하는 문제이다.
위에서 구현한 이항 계수 1, 2의 방식으로 푼다면, dp 배열이 400만 * 400만 이 나오기 때문에, 반드시 메모리 초과가 발생 할 것이다.
따라서 해당 문제에서는 페르마의 소정리를 이용하여 구현해야 한다.
모듈러 연산은 위에 적어 놓았다.
기존 방법은 계산한 값에서 모듈러 연산을 적용해 나머지를 구하였는데,
이번에는 nCr=r!(n−r)!n! 값 자체에 모듈러 연산을 수행할 것이다.
하지만 모듈러 연산에는 나눗셈(분수 꼴)에 대한 연산은 존재하지 않는다. 그렇다면 어떻게 해아할까?
분수를 뒤집어 r!(n−r)!n!=n!×r!(n−r)!−1 로 만들 수 있지 않을까?
그렇다면 n!×r!(n−r)!−1modp = (n!modp)×(r!(n−r)!−1modp)modp 일 것이다.
하지만 r!(n−r)!−1modp 해당 값을 어떻게 구할 수 있을까? r!(n−r)!−1 값은 어쨌든 0.xxx 꼴로 나올 것이다.
여기서 페르마의 소정리를 사용하게 된다.
페르마의 소정리는 다음과 같다.
a는 정수, p는 소수이고 a와 p는 서로소일때,
ap≡a(modp)
이고,
apmodp≡amodp 이다.
또한,
ap−1modp≡1(modp) 이다.
즉, a×ap−2modp≡1(modp)
위 내용으로서, a−1(modp) = ap−2(modp) 이 성립한다.
해당 문제에 적용시켜보면,
a=r!(n−r)!이고, p = 1,000,000,007 이다.
즉, r!(n−r)!−1 = r!(n−r)!1,000,000,007−2이다.
최종적인 식은
n!×r!(n−r)!1,000,000,007−2modp = (n!modp)×(r!(n−r)!1,000,000,007−2modp)modp 이 된다.
헉. 그렇다면 저 큰 제곱은 어떻게 구하나??
곱셈에서 사용한 내용을 보면 쉽다. 해당 문제는 A를 B번 곱했을때 C로 나눈 나머지의 값을 출력한다.
분할 정복 및 모듈러 연산을 사용하여 A를 B번 곱했을때 C로 나눈 나머지의 값을 알 수 있다.
위 방식을 사용하여 구현한다면 다음과 같다.
private static void solution() throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
long N = Long.parseLong(st.nextToken());
long K = Long.parseLong(st.nextToken());
long top = factorial(N);
long bottom = factorial(K) * factorial(N - K) % P;
System.out.println(top * compute(bottom, P - 2) % P);
}
private static long factorial(long N) {
long num = 1L;
while (N > 1) {
num = (num * N) % P;
N--;
}
return num;
}
static long compute(long a, long b) {
if (b == 1) {
return a % P;
}
long tmp = compute(a, b / 2);
if (b % 2 == 1) {
return (tmp*tmp % P) * a % P;
}
return tmp * tmp % P;
}
해당 풀이는 이항 계수 3를 참고하면 된다.