趣味走方格系列(卡特兰数,Lucas定理,逆元)


一:传送门

M * N的方格,左上走到右下,

只能向右或向下走。有多少种不同的走法?

由于方法数量可能很大,只需要输出Mod 10^9 + 7的结果。

Input

第1行,2个数M,N,中间用空格隔开。(2 <= m,n <= 1000)

Input

2 3

Output

3

分析:

n, m 都非常小,可以直接打表递推出来。因为只能 向右走 或 向下走,

a[ i ] [ j ] = a[ i - 1 ][ j ] + a[ i ][ j-1 ]; 当前格的方案数 = 上面格的方案数 + 左面格的方案数。

void DP(int m, int n)
{
    for(int i = 1; i <= m; i++)
        dp[i][1] = 1;
    for(int i = 1; i <= n; i++)
        dp[1][i] = 1;
    for(int i = 2; i <= m; i++)
        for(int j = 2; j <= n; j++)
            dp[i][j] = (dp[i-1][j] + dp[i][j-1])%M;
}

二:传送门

问题同上,只是  m , n 的数据范围都扩大到了  1e6 ~~~

分析:

显然,用递推,打表,时间过不去,内存也存不下。

对于这个表,观察后可以发现,如果顺时针旋转45°,就是一个杨辉三角形~~(行和列都从 0 开始计数

对于一个杨辉三角
行数n 和 每行的个数m 都从0开始计数
对于第 n 行 第m个数 , 它的值为 C(n,m)。。。。

右下角的格子,如果看成杨辉三角的话,是 N  =(n-1)+ (m-1) 行,第  t  =(n-1)或 (m-1) 个

ans = C( N, t )..

现实理论基础的理解为:::向右 向下  一共可以走 N 步,,从中选择 t 步向下(或向右)。。

所以是C(N,t)。

对于逆元。。。。。。。。。。

取模运算的变形中 是没有除法的
也就是说。。
a/b  %  c
如果a,b 特别特别大,不能分别计算a%c   b%c,
所以要用到逆元转换为乘法来算

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int M = 1e9+7;

void exgcd(int a, int b, int &x, int &y)
{
    if(b == 0)
    {
        x = 1, y = 0;
        return ;
    }
    exgcd(b, a%b, x, y);
    int t = x;
    x = y;
    y = t-a/b*y;
    return ;
}

int main()
{
    ios::sync_with_stdio(false);
    int m,n,x,y;
    LL a = 1, b = 1;
    cin >> m >> n;
    n = n+m-2;
    m = m - 1;
    for(int i = n; i >= n-m+1; i--)
        a = (a*i)%M;
    for(int i = m; i >= 1; i--)
        b = (b*i)%M;
    exgcd(b,M,x,y);
    x = (x%M + M) % M;  // 确保x为正
    LL ans = (a*x)%M;
    cout << ans << endl;

    return 0;
}

三:传送门

给出一个 n*n 的矩阵, n 的数据范围是 1e9  , 并且不能穿过对角线

问从左上角到右下角有几种走法。

分析:

不能穿过对角线,且对角线两边是对称的,所以只需要算出一边就行。。

先只看对角线的一边。。只能向下 或者向右运动,经分析,符合卡特兰数。。!!!!!

 

~~~~~~~~~~~~~~~~~~~~卡特兰数~~~~~~~~~~~~~~~~~

如果一个数列 F( n ) 满足科特兰数, 那么

 

应用: 对于一个由 n 个 1 和 n 个 0 ,组成的序列,

使 任意的前缀 1 的个数 >= 0的个数。(条件),这样的序列有多少个。。。

1.不考虑条件的情况下,在 2n 个数中选择 n 个位置放 1 ,那么一共有 C( 2n, n ) 种情况。

2.主要就是要减去其中不符合条件的情况。

3.假设放 ( n + 1) 个 0 , ( n - 1) 个1,0的个数比1的个数多2 .。这样必会出现某个前缀,0的个数大于1的个数。

4.假设在某一位刚好 0 的积累数比 1 的积累数 多 1,那么在这一位的后面的数,0的个数也比1的个数多1,。。

5.如果把 这一位后面的 0 和 1 互换,那么整个序列 0 和 1 的个数就相等了。并且不合法!!!

 

综上所述:每一个由 ( n+ 1 ) 个0 组成的序列,都对应一个 (n) 个0组成的  不合法序列。

所以不合法序列的个数是  C( 2n, n+1)...

总的合法序列的个数为 :    C( 2n, n) - C( 2n, n+1) = C(2n, n) / ( n+1 ) ..

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

而对于本题,n * n 的矩阵 ,把向左走看作 1 , 向下走看作 0,行走的方案就变成了10组成的序列,且10的个数相等。。

如果看对角线上面的部分,第一步肯定是 1,所以问题就转化成了:求 2n长的序列,n个0,n个1,满足任意前缀1

的个数 >= 0的个数的序列的个数,。 最后在 *2 .。就Ok了

这就是卡特兰数了啊····~~~~~!!!!!!!!

这还没有完!!!! 这一题的 n 的范围是 1e9 啊 啊啊啊

如果按照第二题的方法去求组合数就一定会超时的!!!!!!!!

所以就要用超大数求组合数 C( a , b )%M ,  卢卡斯定理~~~~~!!!!!

~~~~~~~~~~~~~~~~~~~~Lucas~~~~~~~~~~~~~~~~~

C(a,b)%M,,
在 a , b 很大的情况下,比如1e15......
但是 M 必须是素数,,
而且实际运用中, M 不能很大,,最多1e5吧
不然GetC() 函数还是容易超时,
表达式:C(a ,b)%M = C( a/M , b/M ) * C(a%M ,b%M ) %M...
实际运用中   a/M   b/M 还是可能很大的,所以一般用递归算法来求解

LL Lucas(LL a, LL b)
{
    if(a < M && b < M)
        return C(a,b);
    return
        C(a%M,b%M) * Lucas(a/M,b/M);
}

因为 M 不是很大,所以组合数就能直接求了

LL GetC(LL a,LL b) //C(a,b)%M
{
    if(b > a) return 0;
    if(b > a-b) b = a-b;   //提高效率
    int n = 1, m = 1;
    int t = a-b+1;
    for(int i = a; i >= t; i--)
        m = (m*i)%M;
    for(int i = b; i >= 2; i--)
        n = (n*i)%M;
    // C(a,b)%M = m/n %M
    // m,n都已经取过模了,所以要用逆元
    exgcd(n,M,x,y);
    x = (x%M + M) % M;
    return m*x % M; 
}

学了卡特兰数 和 卢卡斯定理,这道题就差不多是个模板了,

当然还有一个exgcd 求逆元。这个另外再说。下面是AC代码

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int M = 1e4+7;

void exgcd(LL a, LL b, LL &x, LL &y)
{
    if(b == 0)
    {
        x = 1, y = 0;
        return ;
    }
    exgcd(b, a%b, x, y);
    int t = x;
    x = y;
    y = t-a/b*y;
    return ;
}

LL GetC(LL a, LL b)
{
    if(b > a-b) b = a-b;
    LL x, y;
    LL n = 1, m = 1;
    LL t = a-b+1;
    for(int i = a; i >= t; i--)
        m = (m*i)%M;
    for(int i = b; i >= 2; i--)
        n = (n*i)%M;
    exgcd(n,M,x,y);
    x = (x%M + M) % M;
    return (m*x) % M;
}

LL Lucas(LL a, LL b)
{
    if(a < M && b < M)
        return GetC(a, b);
    return
        (GetC(a%M, b%M) * Lucas(a/M, b/M)) % M;
}

int main()
{
    ios::sync_with_stdio(false);
    LL n,a,b,x,y;
    cin >> n;
    a = 2*n - 2;
    b = n - 1;
    // C(a, b) / n
    int ans = Lucas(a,b);
    // ans/n % M;
    exgcd(n,M,x,y);
    x = (x%M + M) % M;
    cout << 2 * (ans*x) % M << endl;
    return 0;
}

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值