2021年寒假每日一题,2017~2019年的省赛真题。本文内容由倪文迪(华东理工大学计算机系软件192班)和罗勇军老师提供。每日一题,关注蓝桥杯专栏: https://blog.csdn.net/weixin_43914593/category_10721247.html
每题提供C++、Java、Python三种语言的代码。
2019省赛A组第10题“组合数问题” ,题目链接:
http://oj.ecustacm.cn/problem.php?id=1461
https://www.dotcpp.com/oj/problem2303.html
1、题目描述
给 n , m , k n,m,k n,m,k,求有多少对 ( i , j ) (i,j) (i,j)满足 1 ≤ i ≤ n , 0 ≤ j ≤ m i n ( i , m ) 1 ≤ i ≤ n,0 ≤ j ≤ min(i,m) 1≤i≤n,0≤j≤min(i,m)且 C i j ≡ 0 ( m o d k ) C_i^j ≡ 0(mod \ k) Cij≡0(mod k), k k k 是质数。其中 C i j C_i^j Cij是组合数,表示从 i i i个不同的数中选出 j j j 个组成 一个集合的方案数。
输入:第一行两个数 t, k,其中 t 代表该测试点包含 t 组询问,k 的意思与上文中 相同。
接下来 t 行每行两个整数 n, m,表示一组询问。
输出:输出 t 行,每行一个整数表示对应的答案。由于答案可能很大,请输出答 案除以 1 0 9 + 7 10^9 + 7 109+7 的余数。
数据规模:
40%: 1 ≤ k ≤ 100 , 1 ≤ t ≤ 1 0 5 , 1 ≤ n , m ≤ 2000 1 ≤ k ≤ 100, 1 ≤ t ≤ 10^5,1 ≤ n,m ≤ 2000 1≤k≤100,1≤t≤105,1≤n,m≤2000
100%: 1 ≤ k ≤ 1 0 8 , 1 ≤ t ≤ 1 0 5 , 1 ≤ n , m ≤ 1 0 18 1 ≤ k ≤ 10^8, 1 ≤ t ≤ 10^5,1 ≤ n,m ≤ 10^{18} 1≤k≤108,1≤t≤105,1≤n,m≤1018
2、组合数的计算
式子 C i j ≡ 0 ( m o d k ) C_i^j ≡ 0(mod \ k) Cij≡0(mod k)中符号“ ≡ ≡ ≡”的意思是同余,这个式子的意思是 C i j C_i^j Cij能整除 k k k。
同余的概念和题目,参考博文:https://blog.csdn.net/weixin_43914593/article/details/107642766
组合数的定义是: C i j = i ! j ! × ( i − j ) ! C_i^j =\frac{i!}{j!\times(i-j)!} Cij=j!×(i−j)!i!
有多种计算方法。
(1)直接按定义算。因为有大数,用Python写代码。例如计算 C 50 20 C_{50}^{20} C5020,得47129212243960:
temp = 1 #组合数
i,j = 50,20
for p in range(1,i+1): temp *=p #求i!
print(temp)
for p in range(1,j+1): temp //=p #除j!
for p in range(1,i-j+1): temp //=p #再除以(i-j)!
print(int(temp))
https://blog.csdn.net/qq_36477987/article/details/89521273
直接算是不好的,因为阶乘增长极快,例如 12 ! = 479 , 001 , 600 12!=479,001,600 12!=479,001,600。
(2)按递推式计算: c ( i , j ) = c ( i − 1 , j − 1 ) + c ( i − 1 , j ) c(i,j)=c(i-1,j-1)+c(i-1,j) c(i,j)=c(i−1,j−1)+c(i−1,j)。复杂度 O ( n m ) O(nm) O(nm)。例如计算 C 2000 600 C_{2000}^{600} C2000600:
c = [[0 for i in range(2001)] for i in range(2001)] #用于记录组合数c[n][m]
Mod = int(1e9+7)
n,m =2000, 600
for i in range(n+1):
c[i][0]=1
c[i][i]=1
for i in range(1,n+1): #递推计算所有组合数
for j in range(1,m+1):
c[i][j]= (c[i-1][j-1]+c[i-1][j])% Mod;
print(c[n][m])
3、小规模代码
本题有40%的小规模数据, 1 ≤ n , m ≤ 2000 1 ≤ n,m ≤ 2000 1≤n,m≤2000,复杂度 O ( n m ) O(nm) O(nm)够用。下面是C++代码,能得到一点分数。
#include<bits/stdc++.h>
using namespace std;
const int Mod = 1e9+7;
int c[2010][2010]; //记录组合数c[n][m]
int main(){
int n,m,t,k;
cin >> t >> k;
int nn=2000,mm=2000; //小规模
for(int i=0;i<=nn;i++){
c[i][0]=1;
c[i][i]=1;
}
for (int i =1;i<=nn;i++) //打表,提前计算出所有组合数
for(int j =1;j<=mm;j++)
c[i][j]= (c[i-1][j-1]+c[i-1][j]) % k; //直接对k取余
while(t--){
cin >> n >> m;
int ans=0;
for(int j=0;j<=m;j++)
for(int i=j;i<=n;i++)
if(c[i][j] == 0)
ans++;
cout << ans % Mod;
}
return 0;
}
4、lucas定理
当 1 ≤ n , m ≤ 1 0 18 1 ≤ n,m ≤ 10^{18} 1≤n,m≤1018时,显然用上面的递推方法计算组合数 C i j C_i^j Cij是不可能的。
其实学过数论的队员一看本题,就知道它几乎是一道lucas定理的裸题。lucas定理就是求极大的组合数 C n m C_n^m Cnm对p取余。
因为较为复杂,本文不做解析。请参考博文https://www.freesion.com/article/92161191517/