洛谷 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;
}

小结

注意组合数上下两个字母的含义,不要写反了。

posted @ 2020-11-04 08:19  Vanilla_chan  阅读(114)  评论(0编辑  收藏  举报