题解 洛谷P2822 [NOIP2016 提高组] 组合数问题

题面

思路1

考虑题目中所给的式子,只要我们预处理,求解一个组合数就是 \(O(1)\) 的。但是,这样我们是不能接受的,因为值太大了(大约 \(10^{2000}\))。但是我们只需要求一个组合数能不能被 \(k\) 整除,这启发我们转而思考质因数,因为一个数能被另一个数整除,当且仅当除数有的任何一个质因数,被除数都有,且次数也大于等于除数质因数的次数,然后两个数相除就相当于减质因数的次数。于是预处理阶乘的质因数,然后处理出每个组合数对于 \(k\) 对答案的贡献,再二维前缀和,最后 \(O(1)\) 询问。但是这样时间复杂度是 \(O(nmk\times \max\{w(n!)\})\) 的。(\(w(x)\) 表示 \(x\) 的不同质因子个数,\(w(n!)\)\(n=2000\) 时为 \(303\)

考虑到 \(k\) 是不变的,所以可以先读入 \(k\),然后再算,其次可以考虑 \(x!\) 的质因数,由于只需要求是不是被 \(k\) 整除,所以只需要考虑 \(k\) 的质因数在 \(x!\) 中的次数即可。

这样就好了,时间复杂度 \(O(nm \times \max_{i=2}^{i\le21}\{w(i)\})\),小于 \(10^7\),可以的。

#include <bits/stdc++.h>
using namespace std;
const int N=2005;
int k,t,n,m;
vector<int> listk,listn[N];
int p[22],pr[N][22];
int ans[N][N],ak[N];
signed main() {
	cin>>t>>k;
	for(int i=2;i<=k/i;i++) {
		bool flag=0;
		while(k%i==0) {
			k/=i;
			p[i]++;
			flag=1;
		}
		if(flag) listk.push_back(i);
	}
	if(k!=1) listk.push_back(k),p[k]++;
	sort(listk.begin(),listk.end());
	for(int i=2;i<=2000;i++) {
		for(int j=0;j<22;j++) pr[i][j]=pr[i-1][j];
		listn[i]=listn[i-1];
		int tmp=i;
		for(int j=0;j<listk.size();j++) {
			int x=listk[j];
			bool flag=0;
			while(tmp%x==0) {
				tmp/=x;
				pr[i][x]++;
				flag=1;
			}
			if(flag&&!pr[i-1][x]) listn[i].push_back(x);
		}
	}
	for(int i=1;i<=2000;i++) {
		for(int j=1;j<=i;j++) {
			vector<int> now;
			for(auto iter:listn[i]) now.push_back(iter),ak[iter]=pr[i][iter];
			for(auto iter:listn[j]) ak[iter]-=pr[j][iter];
			for(auto iter:listn[i-j]) ak[iter]-=pr[i-j][iter];
			bool flag=1;
			for(auto iter:listk) {
				if(ak[iter]<p[iter]) {
					flag=0;
					break;
				}
			}
			if(flag) ans[i][j]=1;
			for(auto iter:listn[i]) ak[iter]=0;
		}
	}
	for(int i=1;i<=2000;i++)
		for(int j=1;j<=2000;j++)
			ans[i][j]+=ans[i-1][j]+ans[i][j-1]-ans[i-1][j-1];
	while(t--) {
		cin>>n>>m;
		m=min(n,m);
		cout<<ans[n][m]<<endl;
	}
	return 0;
}

思路2

考虑递推组合数,这样是 \(O(nm)\) 的,但是同样是太大了。没事,由于我们求这个数能不能被 \(k\) 整除,可以模 \(k\),最终能不能整除都是一样的,二位前缀和即可。

从这个思路来看,这题评高了。

posted @ 2024-08-26 12:30  PM_pro  阅读(45)  评论(0)    收藏  举报