이항 계수 nCr을 소수 p로 나눈 나머지를 빠르게 구하는 다양한 방법들을 알아보자.
기본적으로 특정 n과 r에 대해서 이항 계수 nCr을 구하는 시간은 O(1)이 되어야 할 때,
즉, 많은 쿼리가 들어와도 문제가 없는 경우를 고려한다.
지금부터 소개하는 방법들의 시간복잡도는 전처리하는데 필요한 시간을 의미한다.
너무 작은 n과 r에 대해서 직접 분자와 분모를 모두 곱하는 경우는 생략한다.
1. O(n2)
11051번: 이항 계수 2
첫째 줄에 N과 K가 주어진다. (1 ≤ N ≤ 1,000, 0 ≤ K ≤ N)
www.acmicpc.net
파스칼의 삼각형을 이용하는 방식이다.
이항 계수에 대한 성질로 매우 잘 알려져있는 nCr=n−1Cr−1+n−1Cr 을 이용해서 dp table을 만든다.
따라서 이를 전처리하기 위해서는 O(n2)에 해당하는 시간과 메모리가 필요하다.
가장 단순한 방법이지만, n과 r이 10000 이상 커진다면 이용하기 힘든 방식이다.
물론 n과 r이 충분히 작다면 이 방식을 이용하지 않을 이유가 없다.
[소스 코드]
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int MOD = (int)1e9 + 7;
ll C[1000][1000];
int main(void) {
int n, r;
for (int i = 1; i <= 1000; i++) {
for (int j = 0; j <= i; j++) {
if (j == 0 || j == i) C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD;
}
}
cout << C[n][r];
}
2. O(nlogp)
11401번: 이항 계수 3
자연수 N과 정수 K가 주어졌을 때 이항 계수 (NK)를 1,000,000,007로 나눈 나머지를 구하는 프로그램을 작성하시오.
www.acmicpc.net
이처럼 n과 r이 400만인 경우는 어떻게 구할까?
위의 방식으로는 400만 x 400만의 배열 크기와 시간이 필요하므로 불가능하다는 것을 당연히 알 수 있다.
페르마의 소정리를 떠올려보자. (링크 : rebro.kr/105)
n이 10000보다 커진다면, 파스칼의 삼각형을 이용하는 방식을 사용할 수 없으므로
nCr=n!/(r!(n−r)!)식을 이용해서 분모와 분자를 직접 계산하는 방식을 이용해줘야 한다.
다만, 우리가 구하는 이항 계수는 p에 대한 나머지를 구하는데, 분모가 존재하므로 계산하기가 쉽지 않다.
따라서 페르마의 소정리인 ap−1≡1(modp)를 이용한다.
이 식을 이용하면 ap−2≡a−1(modp), 즉 a의 modp에 대한 곱셈의 역원이 ap−2인 것을 알 수 있다.
따라서 modp에 대해서 r!(n−r)!을 나누는 것이 아니라, r!(n−r)!의 역원인 (r!(n−r)!)p−2을 곱하는 방식이다.
ak를 구하는데 필요한 시간은 O(logk)이면 충분하다. "분할 정복을 이용한 거듭제곱" 방식이다.
혹시 이를 모른다면 꼭 공부를 하는 것을 권장한다.
예를 들어서 a58을 계산해야 한다고 가정하자. 58을 이진수로 나타내면 111010로 나타난다.
따라서 a58의 지수를 25+24+23+21로 표현하면 a58=a32×a16×a8×a2와 같이 표현이 가능하고,
이진수의 오른쪽 끝부터 시작해서 매번 a를 제곱시켜가면서, 1인 자릿수인 경우에는 결괏값에 곱해주면 된다.
여기까지는 하나의 nCr을 구하는 과정이고, 최종적으로 우리가 원하는 경우는 많은 쿼리가 들어와도 문제가 없는 경우이다.
13977번: 이항 계수와 쿼리
M개의 자연수 N과 정수 K가 주어졌을 때 이항 계수 (NK)를 1,000,000,007로 나눈 나머지를 구하는 프로그램을 작성하시오.
www.acmicpc.net
매번 nCr을 O(1)만에 구하기 위해서는 팩토리얼(factorial) 값에 대해서 미리 전처리를 해두어야 한다.
1!부터 n!까지 배열에 계산을 해두고, 각 팩토리얼값마다 역원을 계산((O(logp))해두면 nCr을 O(1)만에 계산할 수 있다.
코드는 3번에서 확인하자.
3. O(n+logp)
2번 방식에서 약간만 변형시킨 것이다.
원래는 각 팩토리얼값마다 역원을 각각 계산해주었는데, 잘 생각해보면 팩토리얼의 역원에도 점화식이 생긴다.
((n−1)!)−1=n∗(n!)−1 을 만족하기 때문에, 처음에 1!부터 n!까지의 값과 n!의 역원을 미리 계산해두면
1!부터 (n−1)!의 역원까지는 모두 O(1)에 계산할 수 있다.
사실 작정하고 이항계수에 대해서 낸 문제가 아니라면 기본적으로 N이 1000만 이상 주어지지 않기 때문에 보통 대부분의 문제들은 여기까지만 알아도 다 해결할 수 있다.
[소스 코드]
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int MOD = (int)1e9 + 7;
#define MN 4000000
ll fac[MN+10], facinv[MN+10];
ll mpow(ll a, ll x) {
ll res = 1;
while (x > 0) {
if (x % 2) res = (res*a) % MOD;
a = (a*a) % MOD;
x /= 2;
}
return res;
}
void fac_init() {
fac[0] = 1;
for (int i = 1; i <= MN; i++) fac[i] = fac[i - 1] * i % MOD;
facinv[MN] = mpow(fac[MN], MOD - 2);
for (int i = MN - 1; i >= 0; i--) facinv[i] = facinv[i + 1] * (i + 1) % MOD;
}
ll C(ll n, ll r) {
return ((fac[n] * facinv[r]) % MOD) * facinv[n - r] % MOD;
}
int main(void) {
ll n, r;
fac_init();
cout << C(n, r);
}
4. O(n)
O(n+logp)와 비교해서 시간적인 부분은 거의 동일하다.
(실제로 코드 구현에 있어서는 추가적인 연산들로 인해 O(n+logp)의 시간이 덜 소모된다)
다만 구하는 과정이 짧고 꽤나 참신해서 살펴볼만하다.
3번에서 O(logp)가 필요한 이유는 처음에 n!에 대한 역원을 직접 구해줘야 했기 때문이다.
이를 어떻게 O(1)만에 구할 수 있을까?
정수 k에 대한 역원은 DP로 간단하게 구할 수 있고, 점화식은 다음과 같다.
1−1≡1이고, k−1≡−(p/k)×(p%k)−1(modp)
증명 과정을 살펴보자.
먼저 p=(p/k)∗k+p%k 를 만족한다.
양변에 modp를 씌워주면 0≡(p/k)∗k+p%k(modp) , (−p/k)∗k≡p%k(modp) 가 된다.
따라서 ((−p/k)∗k)−1≡(p%k)−1(modp) 를 만족하고,
양변에 (−p/k)를 곱해주면 최종적으로 k−1≡(p%k)−1×(−p/k) 가 나온다.
여기서 나오는 (p/k)는 정수이고, p%k는 항상 k보다 작기 때문에 이전의 dp값으로 계산해줄 수 있다.
[소스 코드]
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int MOD = (int)1e9 + 7;
#define MN 4000000
ll fac[MN+10], facinv[MN+10], inv[MN+10];
void fac_init() {
fac[0] = facinv[0] = inv[1] = 1;
for (int i = 1; i <= MN; i++) {
inv[i+1] = (MOD - MOD / (i+1)) * inv[MOD%(i+1)] % MOD;
fac[i] = fac[i - 1] * i % MOD;
facinv[i] = facinv[i-1]* inv[i] % MOD;
}
}
ll C(ll n, ll r) {
return ((fac[n] * facinv[r]) % MOD) * facinv[n - r] % MOD;
}
int main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
fac_init();
ll n, k; cin >> n >> k;
cout << C(n, k) ;
}
5. O(p)
11402번: 이항 계수 4
첫째 줄에 N, K와 M이 주어진다. (1 ≤ N ≤ 1018, 0 ≤ K ≤ N, 2 ≤ M ≤ 2,000, M은 소수)
www.acmicpc.net
이처럼 n이 매우 큰 경우에는 위에서 설명한 어떤 방법으로도 O(n)보다 작게 계산할 수 없다.
이때, Lucas Theorem (뤼카의 정리)이라는 정리를 사용할 수 있다.
뤼카의 정리는 이처럼 n이 크고 p가 작은 경우에 이항계수를 O(p)만에 구할 수 있는 방법이다.
- Lucas Theorem (뤼카의 정리)
음이 아닌 정수 n과 r, 소수 p에 대해서 n과 r를 p진법으로 나타내면 다음과 같다.
n=nkpk+nk−1pk−1+nk−2pk−2+...+n1p+n0
r=rkpk+rk−1pk−1+rk−2pk−2+...+r1p+r0
이때, nCr≡∏ki=0niCri(modp) 가 성립한다는 것이 뤼카의 정리이다.
증명과정은 다음과 같다.
∑nr=0nCrxr 의 전개과정을 살펴보자.
∑nr=0nCrxr≡(1+x)n≡(1+x)nkpk+nk−1pk−1+...+n1p+n0≡∏ki=0[(1+x)pi]ni를 만족한다.
이때, (1+x)pn은 이항정리에 의해서 pnC0x0+pnC1x1+...+pnCpnxpn 을 만족하고, p가 소수이므로 모든 1≤i≤pn−1에 대해서 pnCi는 p로 나누어 떨어진다.
따라서, (1+x)pn≡1+xpn(modp)를 만족한다.
그러므로 ∏ki=0[(1+x)pi]ni≡∏ki=0[1+xpi]ni(modp) 로 만들어진다.
여기서 다시 안의 식에 이항 정리를 이용하면 ∏ki=0[∑niri=0niCrixripi]가 되고,
[] 안의 식을 전개한 후 정리하면 ∑nr=0[(∏ki=0niCri)xr] 로 만들어진다.
따라서, nCr≡∏ki=0niCri(modp) 가 성립한다.
증명 과정은 복잡하지만, 정리를 코드로 구현하면 생각보다 간단하다.
쿼리당 걸리는 시간은 O(logn/logp) 이므로 상수와 다름없게 생각해도 된다.
[소스 코드]
#include<bits/stdc++.h>
#define MAX_P 2001
using namespace std;
using ll = long long;
const int MOD = 7;
ll fac[MAX_P];
ll mpow(ll a, ll k, ll mod) {
ll res = 1;
while (k > 0) {
if (k % 2) res = (res * a) % mod;
a = (a*a) % mod;
k /= 2;
}
return res;
}
int main(void) {
ll n, r;
ll ans = 1;
while (n || r) {
int N = n % MOD;
int R = r % MOD;
if(R > N) {
ans = 0;
break;
}
ans *= fac[N] * mpow(fac[R] * fac[N - R], MOD - 2, MOD) % MOD;
ans %= MOD;
n /= MOD;
r /= MOD;
}
cout << ans;
}
6. 연습 문제
[BOJ 13977] 이항 계수와 쿼리 Gold I
[BOJ 11402] 이항 계수 4 Platinum V
[BOJ 15791] 세진이의 미팅 Gold I
[BOJ 16134] 조합 Gold I
[BOJ 20296] 폰친구 Platinum IV
[Codeforces 1462E2] Close Tuples (Hard version)
[Codeforces 1445D] Divide and Sum
본 글은 구사과님의 블로그 (koosaga.com/63)에서 영감을 받은 글입니다.
PC로 보시는 것을 권장합니다.
피드백은 언제나 환영입니다. 댓글로 달아주세요 ^-^
'알고리즘 > 수학' 카테고리의 다른 글
PS를 위한 정수론 - (3) 페르마의 소정리와 활용 (이항 계수, 밀러-라빈) (32) | 2021.01.08 |
---|---|
PS를 위한 정수론 - (2) 유클리드, 확장 유클리드 호제법 (0) | 2020.12.29 |
PS를 위한 정수론 - (1) 에라토스테네스의 체 활용 (9) | 2020.12.28 |
소수 판별법 - 에라토스테네스의 체, 밀러-라빈(Miller-Rabin) 소수판별법 (4) | 2020.05.19 |