题面
题目描述 组合数 $C_n^m$ 表示的是从 nn 个物品中选出 mm 个物品的方案数。举个例子,从 (1,2,3)(1,2,3) 三个物品中选择两个物品可以有 (1,2),(1,3),(2,3)(1,2),(1,3),(2,3) 这三种选择方法。根据组合数的定义,我们可以给出计算组合数 $C_n^m$的一般公式: \(C_n^m=\frac{n!}{m!(n-m)!}\) 其中 $n!=1\times2\times\cdots\times n$;特别地,定义 $0!=1$
小葱想知道如果给定 n,m和 k,对于所有的 $0\leq i\leq n,0\leq j\leq \min \left ( i, m \right )$ 有多少对 (i,j)(i,j) 满足 $C_i^j$是 $k$ 的倍数。
输入输出格式
输入格式:
第一行有两个整数 t,k,其中 t 代表该测试点总共有多少组测试数据,k 的意义见问题描述。 接下来 t 行每行两个整数 n,m其中 n,m的意义见问题描述。
输出格式: 共 $t$ 行,每行一个整数代表所有的 $0\leq i\leq n,0\leq j\leq \min \left ( i, m \right )$中有多少对 (i,j)(i,j) 满足 $C_i^j$是 k 的倍数。
样例输入
1 2
3 3
样例输入2
2 5
4 5
6 7
样例1输出
1
样例2输出
0
7
解答
其实看到组合数,应该联想到两个东西:\(C^m_n=C^{m-1}_{n-1}+C^{m}_{n-1}\)
和\(C^m_n=\frac{n!}{m!(n-m)!}\)
对于公式法(第二个),明显是不行的,看数据范围,2000,没搞。并且公式法也没有很好的把已知的数据用起来,造成了一定的信息浪费。好的算法一定信息复用做的很好那么,该怎么办呢?我们看看第一个公式,那不就是杨辉三角???对的!看到组合就一定要和杨辉三角结合起来。于是这道题就要使用杨辉三角来计算!但是又有一个问题了,2000的数据范围,肯定会溢出,怎么办呢?仔细看看题,有多少是k的倍数,有因为mod运算对加减乘是开放的(就是可以稍加思索的乱模),那么我们就可以对k一路进行取模下来,如果结果是c[i][j]=0,那么就说明$C_i^j$是k的倍数。
你以为就完了吗?当然没有!我们看看查询组数,感到了世态炎凉以及GGF对我的智商歧视。但是,我们还有一个神奇的东西:前缀和!应该说是二维前缀和!这样我们提前预处理出一个c数组来存储取模过后的杨辉三角数值,然后用一个s数组来,用s[i][j]表示n=i,m=j的时候的符合条件的数据组数,做到了$O(n^2)$预处理,$O(1)$的查询。下面是代码
# include<iostream>
# include<string.h>
using namespace std;
int c[2015][2015];
int s[2015][2015];
int n,m,k,t;
int main(void) {
ios::sync_with_stdio(false);
cin >> t >> k;
for (int i = 0; i <= 2000; i++)
c[i][0] = c[i][i] = 1;
for (int i = 1; i <= 2000; i++){
for (int j = 1; j < i; j++) {
c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % k;
}
}//上面实在预处理杨慧三角
for(int i = 1;i<=2000;i++){
for(int j = 1;j<=2000;j++){
s[i][j] = s[i-1][j]+s[i][j-1]-s[i-1][j-1];//二维前缀和的使用
if(c[i][j]==0&&j<=i){//如果这个位置也是k的倍数的话,要在s上把自己加上去,注意,j<=i,不然就是没有计算过,肯定是0,这样的话就会出错。
//而i,j都跑到2000是为了防止GGF坑人出现m>n的情况(虽然题面中说了不可能),反正不这么写就不对
s[i][j]++;
}
}
}
while(t--){
cin>>n>>m;
cout<<s[n][m]<<endl;
}
return 0;
}