引入:
组合数C(m,n)表示在m个不同的元素中取出n个元素(不要求有序),产生的方案数。定义式:C(m,n)=m!/(n!*(m-n)!)(并不会使用LaTex QAQ)。
根据题目中对组合数的需要,有不同的计算方法。
(1)在模k的意义下求出C(i,j)(1≤j≤i≤n)共n2 (数量级)个组合数:
运用一个数学上的组合恒等式(OI中称之为杨辉三角):C(m,n)=C(m-1,n-1)+C(m-1,n)。
证明:
1.直接将组合数化为定义式暴力通分再合并。过程略。
2.运用组合数的含义:设m个元素中存在一个“特殊”元素a,对从m个元素中选出n个元素进行分类讨论。
第一种情况:n个元素中含有元素a,则只需在剩余m-1个元素中选出n-1个元素即可。方案数为C(m-1,n-1)。
第二种情况:n个元素中不含元素a,则只需在剩余m-1个元素中选出n个元素即可。方案数为C(m-1,n)。
这样我们就得到了一个与组合数有关的递推式,初始化C(i,0)=1,随后通过递推以O(n2)的复杂度完成计算。均摊O(1)。
例题:NOIP2016 D2T1 组合数问题 题目链接
题意:给定一个数k,然后给出t组m,n,对于每一组数据,询问对于C(i,j)(0≤i≤n,0≤j≤min(i,m)),有多少个C(i,j)是k的倍数。
数据范围:k≤21,m,n≤2000,t≤10000。子任务见题目链接。
题解:
70分做法:O(20002)预处理出所有组合数,然后每次暴力扫描C(i,j)判断是否是k的倍数。然后机智地忘记取模(没错就是我233)
90分做法:在原有70分做法的预处理中加上取模,暴力扫描判断是否为0。
100分做法:发现每次只是数据范围改变,k和组合数都没有改变,所以尝试优化重复操作。
预处理+取模后,问题变为在整张组合数表中某个范围内0的个数。我们将非0数置0,将0置1,问题转化为矩阵和。用前缀和预处理可以做到O(1)查询。
代码:(将近一年前写的 巨丑)
#include<bits/stdc++.h>
using namespace std;
const int maxn=2000+10;
int c[maxn][maxn],d[maxn][maxn],s[maxn][maxn];
int t,n,m,k;
int main()
{
int i,j;
cin>>t>>k;
for(i=0;i<maxn;i++){c[i][0]=c[i][i]=1;}
for(i=1;i<maxn;i++){for(j=1;j<i;j++){c[i][j]=(c[i-1][j-1]+c[i-1][j])%k;}}
for(i=0;i<maxn;i++){for(j=i+1;j<maxn;j++){c[i][j]=1;}}
for(i=0;i<maxn;i++){for(j=0;j<maxn;j++){if(c[i][j]){d[i][j]=0;}else{d[i][j]=1;}}}
for(i=0;i<maxn;i++){s[i][0]=s[i-1][0]+d[i][0];s[0][i]=s[0][i-1]+d[0][i];}
for(i=1;i<maxn;i++){for(j=1;j<maxn;j++){s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+d[i][j];}}
while(t--)
{
int ans=0;
scanf("%d%d",&n,&m);
//for(i=0;i<=n;i++){for(j=0;j<=min(i,m);j++){if(!c[i][j]){ans++;}}}
//cout<<ans<<endl;
printf("%d\n",s[n][m]);
}
return 0;
}