組合數取模在ACM競賽中是一個很重要的問題,很多選手因爲數據太大而束手無策,今天就來詳細講解它。
組合數取模就是求的值,當然根據,和的取值範圍不同,採取的方法也不一樣。
接下來,我們來學習一些常見的取值情況
(1)和
這個問題比較簡單,組合數的計算可以靠楊輝三角,那麼由於和的範圍小,直接兩層循環即可。
(2)和,並且是素數
這個問題有個叫做Lucas的定理,定理描述是,如果
那麼得到
這樣然後分別求,採用逆元計算即可。
題目:http://acm.fzu.edu.cn/problem.php?pid=2020
題意:求,其中,並且是素數。
代碼:
- #include <iostream>
- #include <string.h>
- #include <stdio.h>
- using namespace std;
- typedef long long LL;
- LL n,m,p;
- LL quick_mod(LL a, LL b)
- {
- LL ans = 1;
- a %= p;
- while(b)
- {
- if(b & 1)
- {
- ans = ans * a % p;
- b--;
- }
- b >>= 1;
- a = a * a % p;
- }
- return ans;
- }
- LL C(LL n, LL m)
- {
- if(m > n) return 0;
- LL ans = 1;
- for(int i=1; i<=m; i++)
- {
- LL a = (n + i - m) % p;
- LL b = i % p;
- ans = ans * (a * quick_mod(b, p-2) % p) % p;
- }
- return ans;
- }
- LL Lucas(LL n, LL m)
- {
- if(m == 0) return 1;
- return C(n % p, m % p) * Lucas(n / p, m / p) % p;
- }
- int main()
- {
- int T;
- scanf("%d", &T);
- while(T--)
- {
- scanf("%I64d%I64d%I64d", &n, &m, &p);
- printf("%I64d\n", Lucas(n,m));
- }
- return 0;
- }
由於上題的比較大,所以組合數只能一個一個計算,如果的範圍小點,那麼就可以進行階乘預處理計算了。
(3)和,並且可能爲合數
這樣的話先採取暴力分解,然後快速冪即可。
題目:http://acm.nefu.edu.cn/JudgeOnline/problemshow.php?problem_id=628
代碼:
- #include <iostream>
- #include <string.h>
- #include <stdio.h>
- using namespace std;
- typedef long long LL;
- const int N = 200005;
- bool prime[N];
- int p[N];
- int cnt;
- void isprime()
- {
- cnt = 0;
- memset(prime,true,sizeof(prime));
- for(int i=2; i<N; i++)
- {
- if(prime[i])
- {
- p[cnt++] = i;
- for(int j=i+i; j<N; j+=i)
- prime[j] = false;
- }
- }
- }
- LL quick_mod(LL a,LL b,LL m)
- {
- LL ans = 1;
- a %= m;
- while(b)
- {
- if(b & 1)
- {
- ans = ans * a % m;
- b--;
- }
- b >>= 1;
- a = a * a % m;
- }
- return ans;
- }
- LL Work(LL n,LL p)
- {
- LL ans = 0;
- while(n)
- {
- ans += n / p;
- n /= p;
- }
- return ans;
- }
- LL Solve(LL n,LL m,LL P)
- {
- LL ans = 1;
- for(int i=0; i<cnt && p[i]<=n; i++)
- {
- LL x = Work(n, p[i]);
- LL y = Work(n - m, p[i]);
- LL z = Work(m, p[i]);
- x -= (y + z);
- ans *= quick_mod(p[i],x,P);
- ans %= P;
- }
- return ans;
- }
- int main()
- {
- int T;
- isprime();
- cin>>T;
- while(T--)
- {
- LL n,m,P;
- cin>>n>>m>>P;
- n += m - 2;
- m--;
- cout<<Solve(n,m,P)<<endl;
- }
- return 0;
- }
接下來看一些關於組合數取模的典型題目。
題目:http://acm.hdu.edu.cn/showproblem.php?pid=3944
分析:組合數取模的典型題目,用Lucas定理,注意要階乘預處理,否則會TLE的。
題目:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=4536
題意:給一個集合,一共個元素,從中選取個元素,選出的元素中沒有相鄰的元素的選法一共有多少種?
分析:典型的隔板法,最終答案就是。然後用Lucas定理處理即可。
題目:http://acm.hdu.edu.cn/showproblem.php?pid=4373
題意:個for循環嵌套,有兩種形式,第一類從1開始到,第二類從上一層循環當前數開始到,第一層一定
是第一種類型,求總的循環的次數對364875103取餘的結果。
分析:首先可以看出,每一個第一類循環都是一個新的開始,與前面的狀態無關,所以可以把個嵌套分爲幾個不
同的部分,每一個部分由第一類循環開始,最終結果相乘即可。剩下的就是第二類循環的問題,假設一個
層循環,最大到,分析一下得到如下結果
(1)只有一層,則循環次數爲
(2)只有前兩層,則循環次數爲
(3)只有前三層,則循環次數爲
由此得到結論:第的循環次數爲
代碼:
- #include <iostream>
- #include <string.h>
- #include <stdio.h>
- using namespace std;
- typedef long long LL;
- const int N = 25;
- const int MOD1 = 97;
- const int MOD2 = 3761599;
- const int MOD = MOD1 * MOD2;
- int m,n,k;
- int a[N];
- LL fac1[MOD1+10];
- LL fac2[MOD2+10];
- LL inv1,inv2;
- LL quick_mod(LL a,LL b,LL m)
- {
- LL ans = 1;
- a %= m;
- while(b)
- {
- if(b & 1)
- {
- ans = ans * a % m;
- b--;
- }
- b >>= 1;
- a = a * a % m;
- }
- return ans;
- }
- LL C(LL n,LL m,LL p,LL fac[])
- {
- if(n < m) return 0;
- return fac[n] * quick_mod(fac[m] * fac[n-m], p - 2, p) % p;
- }
- LL Lucas(LL n,LL m,LL p,LL fac[])
- {
- if(m == 0) return 1;
- return C(n % p, m % p, p, fac) * Lucas(n / p, m / p, p, fac);
- }
- void Init()
- {
- fac1[0] = fac2[0] = 1;
- for(int i=1; i<MOD1; i++)
- fac1[i] = (fac1[i-1] * i) % MOD1;
- for(int i=1; i<MOD2; i++)
- fac2[i] = (fac2[i-1] * i) % MOD2;
- inv1 = MOD2 * quick_mod(MOD2, MOD1-2, MOD1);
- inv2 = MOD1 * quick_mod(MOD1, MOD2-2, MOD2);
- }
- int main()
- {
- Init();
- int T, tt = 1;
- scanf("%d",&T);
- while(T--)
- {
- scanf("%d%d%d",&n,&m,&k);
- for(int i=0; i<k; i++)
- scanf("%d",&a[i]);
- a[k] = m;
- LL ans = 1;
- for(int i=0; i<k; i++)
- {
- LL m1 = Lucas(a[i+1] - a[i] + n - 1, a[i+1] - a[i], MOD1, fac1);
- LL m2 = Lucas(a[i+1] - a[i] + n - 1, a[i+1] - a[i], MOD2, fac2);
- LL mm = (m1 * inv1 + m2 * inv2) % MOD;
- ans = ans * mm % MOD;
- }
- printf("Case #%d: ",tt++);
- cout<<ans<<endl;
- }
- return 0;
- }
題目:http://acm.hdu.edu.cn/showproblem.php?pid=4349
題意:中有多少個奇數,其中。
分析:其實組合數判斷奇偶性有一個優美的結論
如果,那麼爲奇數,否則爲偶數
當然本題要判斷的組合數很多,所以不能用上述結論,只能另闢蹊徑。由於是判斷奇偶性,那麼就是判斷
是否爲1,利用Lucas定理,先把和化爲二進制,這樣它們都是01序列了。我們又知道
。這樣中爲0的地方對應的中的位置只有一種可能,那就是0。
這樣我們可以不用管中爲0的地方,只考慮中爲1的位置,可以看出,中爲1的位置對應的中爲0
或1,其結果都是1,這樣答案就是:1<<(二進制表示中1的個數)
代碼:
- #include <iostream>
- #include <string.h>
- #include <stdio.h>
- using namespace std;
- int main()
- {
- int n;
- while(scanf("%d",&n)!=EOF)
- {
- int cnt = 0;
- while (n)
- {
- if (n & 1) cnt++;
- n >>= 1;
- }
- printf("%d\n",1<<cnt);
- }
- return 0;
- }
題目:http://61.187.179.132/JudgeOnline/problem.php?id=1951
題意:給定兩個正整數和,其中,求下面表達式的值
分析:由於999911659是素數,用費馬小定理降冪得到
現在關鍵是求
那麼我們枚舉分別計算,但是模的是合數,所以對999911658進行分解得到
,那麼分別求,即
然後進一步得到同餘方程組爲
再通過中國剩餘定理(CRT)可以求得最終答案。
代碼:
- #include <iostream>
- #include <string.h>
- #include <stdio.h>
- using namespace std;
- typedef long long LL;
- const int P = 999911659;
- LL a[5] = {0, 0, 0, 0};
- LL m[5] = {2, 3, 4679, 35617};
- LL fac[5][36010];
- LL N, G;
- void Init()
- {
- for(int i=0; i<4; i++)
- {
- fac[i][0] = 1;
- for(int j=1; j<36010; j++)
- fac[i][j] = fac[i][j-1] * j % m[i];
- }
- }
- LL quick_mod(LL a,LL b,LL m)
- {
- LL ans = 1;
- a %= m;
- while(b)
- {
- if(b & 1)
- {
- ans = ans * a % m;
- b--;
- }
- b >>= 1;
- a = a * a % m;
- }
- return ans;
- }
- LL C(LL n,LL k,int cur)
- {
- LL p = m[cur];
- if(k > n) return 0;
- return fac[cur][n] * quick_mod(fac[cur][k] * fac[cur][n-k], p - 2, p) % p;
- }
- LL Lucas(LL n,LL k,int cur)
- {
- LL p = m[cur];
- if(k == 0) return 1;
- return C(n % p, k % p, cur) * Lucas(n / p, k / p, cur) % p;
- }
- void extend_Euclid(LL a, LL b, LL &x, LL &y)
- {
- if(b == 0)
- {
- x = 1;
- y = 0;
- return;
- }
- extend_Euclid(b, a % b,x, y);
- LL tmp = x;
- x = y;
- y = tmp - a / b * y;
- }
- LL RemindChina(LL a[],LL m[],int k)
- {
- LL M = 1;
- LL ans = 0;
- for(int i=0; i<k; i++)
- M *= m[i];
- for(int i=0; i<k; i++)
- {
- LL x, y;
- LL Mi = M / m[i];
- extend_Euclid(Mi, m[i], x, y);
- ans = (ans + Mi * x * a[i]) % M;
- }
- if(ans < 0)
- ans += M;
- return ans;
- }
- int main()
- {
- Init();
- while(cin>>N>>G)
- {
- a[0] = a[1] = 0;
- a[2] = a[3] = 0;
- if(G == P)
- {
- cout<<"0"<<endl;
- continue;
- }
- G %= P;
- for(int i=1; i*i <= N; i++)
- {
- if(N % i == 0)
- {
- LL x = i;
- a[0] = (a[0] + Lucas(N, x, 0)) % m[0];
- a[1] = (a[1] + Lucas(N, x, 1)) % m[1];
- a[2] = (a[2] + Lucas(N, x, 2)) % m[2];
- a[3] = (a[3] + Lucas(N, x, 3)) % m[3];
- x = N / i;
- if(i * i != N)
- {
- a[0] = (a[0] + Lucas(N, x, 0)) % m[0];
- a[1] = (a[1] + Lucas(N, x, 1)) % m[1];
- a[2] = (a[2] + Lucas(N, x, 2)) % m[2];
- a[3] = (a[3] + Lucas(N, x, 3)) % m[3];
- }
- }
- }
- LL ans = quick_mod(G, RemindChina(a, m, 4), P);
- cout<<ans<<endl;
- }
- return 0;
- }
題目:已知有如下表達式
給定和,求。
分析:如果直接二項式展開,這樣會很麻煩,而且不容易求出,本題有技巧。做如下變換
所以問題變爲求的值。