数论4 组合数
目录
前言
求法一
代码
求法二
代码
求法三
代码
求法四
代码
前言
今天要将最后一部分,主要涉及组合数的四种求法。
前置知识
组合数的通项公式:
组合数的递推公式:
卢卡斯定理:
我们今天需要求的四种求法主要基于这几个公式。
求法一
求法一利用的是递推公式,主要用于n <= 2000
即可以打n^2
的表的题目。
这个递推公式是怎样求出来的呢?其实很简单,我们单独对一个元素做分类讨论,显然有两种可能的情况:
-
选择:
C_{n - 1}^{m - 1}
-
不选择:
C_{n - 1} ^ {m}
最后二者相加能够得到的就是C_n^m
,时间复杂度是O(n^2)
。
代码
const int N = 2010;
int C[N][N];
void init()
{
for (int i = 0; i < N; i++)
{
C[i][0] = 1;
for (int j = 1; j <= i; j++)
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % P;
}
}
求法二
求法二主要应对的是n <= 1e5
这种只能够打一维表的数据。我们使用通项公式求解。不过需要取模,而对于模意义下显然不可以直接做除法,需要求逆元。
注意这里有个隐藏条件是P
是质数,这样才能保证每一项都存在逆元。时间复杂度为O(nlogn)
代码
const int N = 100010, P = 10007;
int F[N], Fe[N];
int quick(int n, int k)
{
int cnt = 1;
while (k)
{
if (k & 1) cnt = cnt * n % P;
n = n * n % P;
k >>= 1;
}
return cnt;
}
void init()
{
F[0] = Fe[0] = 1;
for (int i = 1; i < N; i++)
{
F[i] = F[i - 1] * i % P;
Fe[i] = Fe[i - 1] * quick(i, P - 2) % P;
}
}
求法三
可算是找到了一道模板题(
这道题可以发现n
很大,若再使用阶乘求得话时间复杂度就是nlogn
是必定超时的。
对于这种情况我们就可以使用卢卡斯定理求解,当然依旧有个前提是P
是质数。
至于这个卢卡斯定理怎么求出来的我也不太懂,大家记住就好了,很形象,不难记。
若使用卢卡斯定理的话时间复杂度是:
代码
#include<iostream>
using namespace std;
const int P = 10007;
int t, m, n;
int quick(int n, int k)
{
int cnt = 1;
while(k)
{
if(k & 1) cnt = cnt * n % P;
k >>= 1;
n = n * n % P;
}
return cnt;
}
int C(int n, int m)
{
int l = 1;
for(int i = 1, j = n; i <= m; i++, j--)
{
l = l * j % P;
l = l * quick(i, P - 2) % P; //乘以逆元
}
return l;
}
int lks(int n, int m)
{
if(n < P) return C(n, m);
return C(n % P, m % P) * lks(n/P, m/P) % P;
}
int main()
{
scanf("%d", &t);
while(t--)
{
scanf("%d%d", &n, &m);
printf("%d\n", lks(n, m));
}
}
求法四
n
很大并且不取余。
显然对于这样的问题就需要使用高精度来求解。
我们使用的依旧是通项公式。
不过对于通项公式:
里面是有乘法有除法的,这并不理想,有没有更优秀的求法呢?
这个求法很巧妙的,原理就是将阶乘分解质因数,然后消掉重复的部分,因为组合数都是整数所以上面部分是一定可以被下面部分整除的,所以大家不需要考虑消不完的情况。
那么我们如何对阶乘分解质因数呢?这个很简单,我们首先筛出所1~n
中所有的质数,随后运用一个公式:
对于这个公式主播最开始感觉有些摸不着头脑,但是想明白后只想说:妙,太妙了……
如何来理解呢?其实很简单n/p
其实就是求出1 ~ n
中能被p
整除的数字的个数,以此类推……
代码
#include<iostream>
#include<vector>
//#define int long long
using namespace std;
//typedef long long LL;
const int N = 1e6 + 10;
bool is_prime[N];
int n, m;
int quick(int n, int k)
{
int l = 1;
while (k)
{
if (k & 1) l *= n;
n = n * n;
k >>= 1;
}
//printf("%d\n", l);
return l;
}
vector<int> prime_shai(int n) //线性筛法
{
vector<int> p;
for (int i = 2; i <= n; i++)
{
if (!is_prime[i]) p.push_back(i);
for (int j = 0; p[j] <= n / i; j++)
{
is_prime[i * p[j]] = true;
if (i % p[j] == 0) break;
}
}
return p;
}
int get_num(int n, int p)
{
int l = 0;
int cnt = p;
while (n / p)
{
l += n / p;
p *= cnt;
}
return l;
}
vector<int> cur(vector<int>& A, int b)
{
int x = 0;
vector<int> C;
for (int i = 0; i < A.size(); i++)
{
x += A[i] * b;
C.push_back(x % 10);
x /= 10;
}
while (x)
{
C.push_back(x % 10);
x /= 10;
}
return C;
}
int main()
{
scanf("%d%d", &n, &m);
vector<int> A = { 1 }; //高精度
vector<int> p = prime_shai(n);
for (int i = 0; i < p.size(); i++)
{
int l = get_num(n, p[i]) - get_num(m, p[i]) - get_num(n - m, p[i]);
A = cur(A, quick(p[i], l));
}
for (int i = A.size() - 1; i >= 0; i--)
printf("%d", A[i]);
return 0;
}