洛谷 P2822 组合数问题
题意简述
已知组合数(从\(n\)个物品中选择\(m\)个物品的方案数)
\[C^m_n=C(n,m)=\binom{n}{m}=\frac{n!}{m!(n-m)!}
\]
其中
\[n! = 1 \times 2 \times 3 \times \cdots \times n
\]
\(Specially\),\(define 0!=1\)
给定\(n\),\(m\),\(k\),对于所有的\(0 \le i \le n\),\(0 \le j \le min(i,m)\),有多少对\((i,j)\)满足\(k \mid {C_i^j}\)
题意转化
求
\[\sum_{i=0}^n \sum_{j=0}^{min(i,n)} [k \mid C_i^j]
\]
这个求和的方式就非常清晰了。那么我们现在只用关系一下几个问题:
时间
看到\(n\),\(m\)最大只有\(2\times10^3\),但是\(t \le 10^4\),不难想到打表。
那么怎么递推地求\(C_i^j\)呢?组合数有一个重要的性质:
\[C_n^m=C_{n-1}^m+C_{n-1}^{m-1}
\]
可以从两个方面给出解释:
感性理解
从\(n\)个物品中拿出\(m\)个物品的方案数,相当于从\(n-1\)个物品中拿出\(m-1\)个物品(第\(n\)个物品不拿)的方案数+从\(n-1\)个物品中拿出\(m\)个物品(拿上第\(n\)个)的方案数
理性推导
\[\begin{split}
C_{n-1}^{m-1}+C_{n-1}^{m}&=\frac{(n-1)!}{(m-1)!(n-1-m+1)!} + \frac{(n-1)!}{m!(n-1-m)!}\\
&=(n-1)! \times \frac{m+(n-m)}{m!(n-m)!}\\
&=\frac{n!}{m!(n-m)!}\\
&=C^m_n
\end{split}
\]
精度
时间上可以过去了,但是组合数会爆\(\texttt{long long}\)啊!
这个也很简单解决
因为k是不变的,利用取模运算的加法原理,即\((a \% p+b\%p)\%p=(a+b)\%\),我们可以知道,我们想要知道的仅仅是这个数模\(k\)剩多少,用余数递推也是可以的。
最后用二位前缀和保存一个区间内可以将\(k\)整除的组合数的个数即可。
Code
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<queue>
#include<vector>
#define IL inline
#define re register
#define LL long long
#define ULL unsigned long long
#define re register
#define debug printf("Now is %d\n",__LINE__);
using namespace std;
template<class T>inline void read(T&x)
{
char ch=getchar();
while(!isdigit(ch))ch=getchar();
x=ch-'0';ch=getchar();
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
}
inline int read()
{
int x=0;
char ch=getchar();
while(!isdigit(ch))ch=getchar();
x=ch-'0';ch=getchar();
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x;
}
int G[55];
template<class T>inline void write(T x)
{
int g=0;
if(x<0) x=-x,putchar('-');
do{G[++g]=x%10;x/=10;}while(x);
for(re int i=g;i>=1;--i)putchar('0'+G[i]);putchar('\n');
}
int t,k;
LL C[2010][2010],s[2010][2010];
void pre()
{
//C[i][j]=C[i-1][j-1]+C[i-1][j]
C[0][0]=C[1][0]=C[1][1]=1;
for(int i=2;i<=2000;i++)
{
C[i][0]=1;
for(int j=1;j<=2000;j++)
C[i][j]=C[i-1][j-1]+C[i-1][j],C[i][j]%=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]) s[i][j]++;
}
s[i][i+1]=s[i][i];
}
}
int main()
{
t=read();
k=read();
pre();
while(t--)
{
int i=read();
int j=read();
cout<<s[i][min(j,i)]<<endl;
}
return 0;
}
小结
注意组合数上下两个字母的含义,不要写反了。