Loading

【Luogu P2282】【JZOJ 4906】【NOIP2016提高组复赛】组合数问题 题解

【Luogu P2282】【JZOJ 4906】【NOIP2016提高组复赛】组合数问题 题解


题面入口--->点这里


题解&&思路

题意简析

 题面上已经描述的很清楚了,就是求一些组合数中有几个为k的倍数。

暴力做法

 对于每一个询问n,m,直接按照题意枚举i,j求出C(i,j)%k的值,统计答案。
 考虑组合数的求法,就我所知有3种:

  1. 根据组合数的计算公式直接计算。复杂度\(O(n)\),但是数很大的时候没法做。
  2. 利用组合数递推式进行计算,这个递推式就是\(C_n^m=C_{n-1}^m+C_{n-1}^{m-1}\),也就是杨辉三角求组合数。
  3. 针对方法1的优化,利用逆元求\(C_n^m mod k\)
     但是方法1和方法3都不是我们关注的重点,因为枚举n,m的复杂度为\(O(tnm)\),必定超时。

正解思路

 那么有名到小学生都知道的杨辉三角到底能对这题起到什么帮助呢?如果使用杨辉三角,就能在\(O(nm)\)预处理后\(O(1)\)回答所有\(C_n^m mod k\)的问题。观察这道题,k是给定的,于是我们可以预处理每个组合数mod k的值,若其为0,说明这个组合数是k的倍数。
 但是还是要\(O(nm)\)枚举,解决不了问题,怎么办呢。
 考虑这样一些式子:
n=1,m=1时ans=\(C_1^1\)
n=2,m=1时ans=\(C_1^1+C_2^1\)
n=2,m=2时ans=\(C_1^1+C_2^1+C_2^2\)
...
 每个答案都包含了前面的答案!那么这个包含的递推式是?我们设f[i][j]为n=i,m=j时的答案,递推式就是:

\[f_i_,_j=f_{i-1}_,_j+f_i_,_{j-1}-f_{i-1}_,_{j-1} \]

 其实就是前缀和。当\(C_i^j mod k = 0\)时f[i][j]要+1。于是就能在\(O(nm)\)预处理下\(O(1)\)回答询问了。
 注意一下细节问题,代码中有体现。

#include <cmath>
#include <cstdio>
#include <cstring>
#include <cstdlib>

const int N = 2007;

int t, k, n, m;
long long f[N][N], sum[N][N]; //不开long long翻车

int main()
{
	freopen("problem.in", "r", stdin); //无视文件操作
	freopen("problem.out", "w", stdout);

	scanf("%d%d", &t, &k);
	f[0][0] = 1;
	for (int i = 1; i <= 2000; i++)
	{
		f[i][0] = 1; //杨辉三角的第0列也就是左边一列全为1,组合数定义C(n,m)=1 (n≠0,m=0)
		for (int j = 1; j <= i; j++)
		{
			f[i][j] = (f[i - 1][j] + f[i - 1][j - 1]) % k; //边递推边取模,因为我们只关心C(n,m)%k的值
			if (f[i][j] == 0) sum[i][j]++; //f[i][j] % k == 0说明C(i,j)是k的倍数
		}
	}
	for (int i = 1; i <= 2000; i++)
		for (int j = 1; j <= i; j++)
			if (i == j) sum[i][j] += sum[i][j - 1]; //特判,不加会错
			else sum[i][j] += sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1]; //前缀和统计答案
	while (t--)
	{
		scanf("%d%d", &n, &m);
		if (m > n) m = n; //如上文所述
		printf("%lld\n", sum[n][m]);
	}

	fclose(stdin);
	fclose(stdout);
	return 0;
}
posted @ 2018-01-24 23:05  gz-gary  阅读(216)  评论(0编辑  收藏  举报