组合数问题(NOIP2016提高组Day2T1)
Time Limit:1000MS Memory Limit:512000K
【题目描述】
组合数表示的是从n个物品中选出m个物品的方案数。举个例子,从(1,2,3) 三个物品中选择两个物品可以有(1,2),(1,3),(2,3)这三种选择方法。根据组合数的定 义,我们可以给出计算组合数的一般公式:
其中n! = 1×2×···×n
小葱想知道如果给定n,m和k,对于所有的0<=i<= n,0<=j<= min(i,m)有多少对 (i,j)满足是k的倍数。
【输入格式】
第一行有两个整数t,k,其中t代表该测试点总共有多少组测试数据,k的意义见【问题描述】。
接下来t行每行两个整数n,m,其中n,m的意义见【问题描述】。
【输出格式】
t行,每行一个整数代表答案。
【输入样例1】
1 2
3 3
【输出样例1】
1
【输入样例2】
2 5
4 5
6 7
【输出样例2】
0
7
【数据范围】
首先明显地,这题我们要求组合数,那就必须用到组合数的递推公式:C(i,j)=C(i-1,j)+C(i-1,j-1),其实就是杨辉三角的递推式。
因为要满足是k的倍数,因此枚举所有范围内的组合数,对k取模就可以直接判断了。
*以下,满足条件的组合数指能被k整除的组合数(既是k的倍数的组合数个数)
不过,为了提高效率,我们可以进行进一步的优化,就是预处理出组合数从而求出所有区间的满足条件的组合数个数,这里就要用到二维前缀和。
设a[i][j]为在C([1,i](从1到i),[1,j](从1到j))内满足条件的组合数个数,初始时,在算组合数C(i,j)时对其mod k,若得0则被k整除,a[i][j]=1;在递推时,类似于一维前缀和,a[i][j]应当对于i和j分别递推,即传递a[i-1][j]和a[i][j-1]的值,由容斥原理可得:a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];这是因为a[i-1][j]和a[i][j-1]都包含了a[i-1][j-1],因此a[i-1][j-1]被加了两次,故要减去一次。
代码:
#include
#include
using namespace std;
int t,k;
int a[2001][2001];
int ans[2001][2001];
int n[10001],m[10001];
int main()
{
int x=0;
scanf("%d%d",&t,&k);
for (int i=1;i<=t;i++)
{
scanf("%d%d",&n[i],&m[i]);
if (n[i]>x) x=n[i]; //只要算出所询问的最大范围内的组合数即可,效率优化
}
for (int i=1;i<=x;i++)
{
a[1][i]=i%k;
if (a[1][i]==0) ans[1][i]++;
}
for (int i=2;i<=x;i++) //组合数递推过程
for (int j=2;j<=i;j++)
{
a[j][i]=(a[j-1][i-1]+a[j][i-1])%k;
if (a[j][i]==0) ans[j][i]++;
}
for (int i=1;i<=x;i++) //二维前缀和递推过程
for (int j=1;j<=x;j++)
ans[j][i]+=ans[j-1][i]+ans[j][i-1]-ans[j-1][i-1];
for (int i=1;i<=t;i++) printf("%d\n",ans[min(n[i],m[i])][n[i]]);
}