这道题的算法含精量特别高,而且十分有趣。如果你会了这道题,那么你的排列算法就很厉害了。
一.算法预备
1.筛法
有托筛和欧筛,反正就是把所有素数给找出来,并存在一起。
(1)埃筛(慢一些)
void seive (int n){
for (int i = 2; i <= n; i ++){
if (! vis[i]){
prime[++ pn] = i;
for (int j = i * 2; j <= n; j += i)
vis[j] = 1;
}
}
}
(2)欧筛(快一些)
void seive (int n){
for (int i = 2; i <= n; i ++){
if (! vis[i])
prime[++ pn] = i;
for (int j = 1; j <= pn && i * prime[j] <= n; j ++){
vis[i * prime[j]] = 1;
if (i % prime[j] == 0)
break;
}
}
}
2. 预处理阶乘法
就是预处理出每一个素数的阶乘(等一下在题目中讲)。
3.逆元
逆元广泛用于除运算和模运算同时出现的地方。因为不能边除边模,所以要用逆元,即:
代码:
LL niyuan (LL n){//快速幂+逆元
LL ans = 1, y = p - 2;
while (y > 0){
if (y % 2 == 1)
ans = ans * n % p;
n = n * n % p;
y /= 2;
}
return ans;
}
4. 组合
这是大家很熟悉的吧,就是数学中的排列组合中的组合
long long C (int x, int k){
long long sum = 1;
if (k > x / 2)
k = x - k;
for (register int i = k; i >= 1; i --)
sum = sum * (x - i + 1) / (k - i + 1) % mod;
return sum;
}
5.卢卡斯定理 Lucas
这是对于组合数算法的优化,十分有用:
LL Lucas (int n, int m){
if (n == m || ! m)
return 1;
return C (n % p, m % p) * Lucas (n / p, m / p) % p;
}
二.题目
1.题目描述
众所皆知杨辉三角形。我们从上到下对行进行编号0,1,2,…,从左到右对列进行编号0,1,2,…。如果使用c(n,k)表示行数n,列数k。杨辉三角形有如下规则图案。
c(n,0)=c(n,n)=1(n≥0)
c(n,k)=c(n-1,k-1)+c(n-1,k)(0<k<n)
编写一个程序,计算从顶部开始到第n行第k列结束的路线上通过的最小数字总和。每个步骤可以是直接向下或对角向下到右侧,如图2所示。
由于答案可能非常大,您只需要输出答案mod p,这是一个素数。
2.输入
问题的输入将由一系列多达100000个数据集组成。对于每个数据,有一行包含三个整数n,k(0<=k<=n<10^9)p(p<10^4,p是素数)。输入被文件结尾终止。
3.输出
对于每个测试用例,您应该输出“case c:”首先,其中c表示用例编号,从1开始。然后输出最小和mod p。
4.样例输入
1 1 1
4 2 2
5.样例输出
Case #1: 0
Case #2: 5
6.提示
建议大家一开始就让n和k都加1!
三.思路与解法
大家首先来熟悉一下杨辉三角形:第n行第m列的数为
好,现在,我们来想一下这个路径该怎么走?大家可以试着去走一下,就会发现路径总是这样的:
或
再来,因为杨辉三角具有对称性,所以的路径等价于的路径,也就是说,走到第4排第3列等价于走到第4排第2列:等价于
所以每次我们都只需找到目标点在杨辉三角性左半边的的对应点即可。
现在来推路径总花费:首先,我们要先走到第n-k+1排第1列的1才开始向内走,一直走到第n行第k列,也就是:(n-k是前面经过的1的总和)
因为,所以可以合并为我们的重点就是,这个东西用卢卡斯定理就行了。
但是,这样还会超时,我们要用预处理阶乘法,算出10^4范围内的所有素数的阶乘即可。(注意每个数的零次方为1!)
void prepare (){//fac[i][j]数组存第i个素数,从1开始,乘j次,如f[4][5]=1*2*3*4*5
seive (10000);
for (int i = 1; i <= pn; i ++){
fac[i][1] = 1;
fac[i][0] = 1;
for (int j = 2; j <= prime[i]; j ++){
fac[i][j] = fac[i][j - 1] * j % prime[i];
}
}
}
四.代码
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
#define M 10005
#define LL long long
LL fac[M / 5][M];
int x, k, p, t, prime[M], pn, p1;
bool vis[M];
void seive (int n){//筛法
for (int i = 2; i <= n; i ++){
if (! vis[i])
prime[++ pn] = i;
for (int j = 1; j <= pn && i * prime[j] <= n; j ++){
vis[i * prime[j]] = 1;
if (i % prime[j] == 0)
break;
}
}
}
void prepare (){//预处理阶乘法
seive (10000);
for (int i = 1; i <= pn; i ++){
fac[i][1] = 1;
fac[i][0] = 1;
for (int j = 2; j <= prime[i]; j ++){
fac[i][j] = fac[i][j - 1] * j % prime[i];
}
}
}
LL niyuan (LL n){//逆元
LL ans = 1, y = p - 2;
while (y > 0){
if (y % 2 == 1)
ans = ans * n % p;
n = n * n % p;
y /= 2;
}
return ans;
}
LL C (int n, int m){//组合
if( n < m ) return 0;
LL ans = fac[p1][n] * niyuan(fac[p1][n - m] * fac[p1][m] % p) % p;
return ans;
}
LL Lucas (int n, int m){//卢卡斯定理
if (n == m || ! m)
return 1;
return C (n % p, m % p) * Lucas (n / p, m / p) % p;
}
int main (){
prepare ();
while (~scanf ("%d %d %d", &x, &k, &p)){
for (int i = 1; i <= pn; i ++)
if (prime[i] == p){
p1 = i;
break;
}
x ++; k ++;
t ++;
if (k > x / 2)
k = x - k + 1;
LL ans = (Lucas (x, k - 1) % p + x - k ) % p;//计算路径
printf ("Case #%d: %I64d\n", t, ans);
}
return 0;
}
谢谢!