【Luogu P2282】【JZOJ 4906】【NOIP2016提高组复赛】组合数问题 题解
题面入口--->点这里
题解&&思路
题意简析
题面上已经描述的很清楚了,就是求一些组合数中有几个为k的倍数。
暴力做法
对于每一个询问n,m,直接按照题意枚举i,j求出C(i,j)%k的值,统计答案。
考虑组合数的求法,就我所知有3种:
- 根据组合数的计算公式直接计算。复杂度(O(n)),但是数很大的时候没法做。
- 利用组合数递推式进行计算,这个递推式就是(C_n^m=C_{n-1}^m+C_{n-1}^{m-1}),也就是杨辉三角求组合数。
- 针对方法1的优化,利用逆元求(C_n^m mod k)。
但是方法1和方法3都不是我们关注的重点,因为枚举n,m的复杂度为(O(tnm)),必定超时。
正解思路
那么有名到小学生都知道的杨辉三角到底能对这题起到什么帮助呢?如果使用杨辉三角,就能在(O(nm))预处理后(O(1))回答所有(C_n^m mod k)的问题。观察这道题,k是给定的,于是我们可以预处理每个组合数mod k的值,若其为0,说明这个组合数是k的倍数。
但是还是要(O(nm))枚举,解决不了问题,怎么办呢。
考虑这样一些式子:
n=1,m=1时ans=(C_1^1)
n=2,m=1时ans=(C_1^1+C_2^1)
n=2,m=2时ans=(C_1^1+C_2^1+C_2^2)
...
每个答案都包含了前面的答案!那么这个包含的递推式是?我们设f[i][j]为n=i,m=j时的答案,递推式就是:
[f_i_,_j=f_{i-1}_,_j+f_i_,_{j-1}-f_{i-1}_,_{j-1}
]
其实就是前缀和。当(C_i^j mod k = 0)时f[i][j]要+1。于是就能在(O(nm))预处理下(O(1))回答询问了。
注意一下细节问题,代码中有体现。
#include <cmath>
#include <cstdio>
#include <cstring>
#include <cstdlib>
const int N = 2007;
int t, k, n, m;
long long f[N][N], sum[N][N]; //不开long long翻车
int main()
{
freopen("problem.in", "r", stdin); //无视文件操作
freopen("problem.out", "w", stdout);
scanf("%d%d", &t, &k);
f[0][0] = 1;
for (int i = 1; i <= 2000; i++)
{
f[i][0] = 1; //杨辉三角的第0列也就是左边一列全为1,组合数定义C(n,m)=1 (n≠0,m=0)
for (int j = 1; j <= i; j++)
{
f[i][j] = (f[i - 1][j] + f[i - 1][j - 1]) % k; //边递推边取模,因为我们只关心C(n,m)%k的值
if (f[i][j] == 0) sum[i][j]++; //f[i][j] % k == 0说明C(i,j)是k的倍数
}
}
for (int i = 1; i <= 2000; i++)
for (int j = 1; j <= i; j++)
if (i == j) sum[i][j] += sum[i][j - 1]; //特判,不加会错
else sum[i][j] += sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1]; //前缀和统计答案
while (t--)
{
scanf("%d%d", &n, &m);
if (m > n) m = n; //如上文所述
printf("%lld
", sum[n][m]);
}
fclose(stdin);
fclose(stdout);
return 0;
}