小黄的刷题之路(八)——码题集OJ赛-组合数取模和Lucas定理

题目

在这里插入图片描述

例如

2
0  0
2  1
------------
1
0

分析和思路

每一行的头和尾是1,每一行的其他位置,如果正上方和左上角中只有一个1,则为1,否则为0

再仔细看这句话:如果 ( n 一 1 , i ) (n 一1,i) (n1,i) ( n ― 1 , i − 1 ) (n ―1,i-1) (n―1,i1)之中只有一个1,那么 ( n , i ) (n,i) (n,i)是1,否则是0

可以理解为 ( n , i ) = [ ( n − 1 , i ) + ( n − 1 , i − 1 ) ] % 2 (n,i)=[(n-1,i)+(n-1,i-1)]\%2 (n,i)=[(n1,i)+(n1,i1)]%2

我们都知道组合数有一个公式: ( n + 1 k ) = ( n k ) + ( n k − 1 ) {n+1 \choose k}={n \choose k}+{n \choose k-1} (kn+1)=(kn)+(k1n) C n + 1 k = C n k + C n k − 1 C_{n+1}^{k}=C_{n}^{k}+C_{n}^{k-1} Cn+1k=Cnk+Cnk1

如果看成组合数,那么就有 [ ( n − 1 , i ) + ( n − 1 , i − 1 ) ] % 2 = ( n , i ) % 2 [(n-1,i)+(n-1,i-1)]\%2=(n,i)\%2 [(n1,i)+(n1,i1)]%2=(n,i)%2

因此转化为一个组合数取模的问题

利用高中教过的组合数公式 C n m = n ! m ! × ( n − m ) ! C_{n}^{m}=\frac{n!}{m!\times (n-m)!} Cnm=m!×(nm)!n!,想偷懒用python的math库写

import math
def main():
    t = int(input())
    while t:
        t = t - 1
        n, m = map(int, input().split())
        if m > n and m != 0:
            ans = 0
        elif m == 0:
            ans = 1
        else:
            ans = math.factorial(n) // (math.factorial(m) * math.factorial(n - m)) % 2
        print(ans)
    pass
if __name__ == '__main__':
    main()

放到平台上运行,毫不意外地超时了,也是,绝对没有那么简单

经过一番查询,发现由于这里的模2是一个很小的质数,且很可能问题规模很大,所以不能通过简单的递推公式求解。下面介绍一下大名鼎鼎的卢卡斯定理

在这里插入图片描述

上图来自卢卡斯定理 - OI Wiki (oi-wiki.org)

实现:前一项递归调用,而后面一项算出组合数

// C++ Version
long long Lucas(long long n, long long m, long long p) {
  if (m == 0) return 1;
  return (Lucas(n / p, m / p, p) * C(n % p, m % p, p)) % p;
}
# Python Version
def Lucas(n, m, p):
  if m == 0:
      return 1
  return (Lucas(n // p, m // p, p) * C(n % p, m % p, p)) % p

接下来的重点是计算组合数,由于自己学识尚浅,只知道杨辉三角的递推公式 C n m = C n − 1 m + C n − 1 m − 1 C_{n}^{m}=C_{n-1}^{m}+C_{n-1}^{m-1} Cnm=Cn1m+Cn1m1

(因为很好记,从n个数中选m个数,只有两种情况:①第n个数不选,从 n − 1 n-1 n1个数中选m个数,即 C n − 1 m C_{n-1}^{m} Cn1m;②第n个数选,则相当于从 n − 1 n-1 n1个数中选 m − 1 m-1 m1个数,即 C n − 1 m − 1 C_{n-1}^{m-1} Cn1m1。那么总方案数即为两者之和)

但是后来发现这个方法就算用滚动数组节省空间,其时间复杂度 O ( n 2 ) O(n^2) O(n2),还是很容易超时

最终参考了大佬的的方法:费马小定理求乘法逆元 + 快速幂求解组合数

计算组合数的几种方法总结_liuzibujian的博客_(C++)

Python快速求组合数C(n,m)三种方法整理_果冻er的博客(python)

代码实现

C++
#include<bits/stdc++.h> 
//typedef long long ll;
//这里尝试了一下int不会爆,如果发现问题规模过大,int无法承载爆了就需要改成long long 
using namespace std;
int p = 2;//素数模为2
int quickpow(int a,int b)//快速幂,求a的b次方
{
    int res = 1;
    while(b)
    {
        if(b%2!=0)res = res*a % p;
        a=a*a % p;
        b >>= 1;	//右移一位
    }
    return res;
}
int C(int n,int m)
{
    if(m>n)return 0;//若y>x输出0
    int a=1,b=1;
    for(int i=n-m+1;i<=n;++i)
        a=a*i%p;
    for(int i=2;i<=m;++i)
        b=b*i%p;
    return a*quickpow(b,p-2)%p;
}
int Lucas(int n,int m)//求C(n,m)%2
{
    if(!m)return 1;//y=0,第0列都是1
    else return (Lucas(n/p,m/p) * C(n%p,m%p))%p;//递归
} 

int main()
{
    int x,y;
    int t;
    cin>>t;//测试样例组数
    while(t--)
    {
        cin>>x>>y;//坐标
        cout<<Lucas(x,y)<<endl;//坐标对应元素(1/0)
    }
    return 0;
}
python

python的组合数计算我调用了math里求阶乘的方法factorial()

import math
def power(x,y):     #求x的y次方
    p = 2
    res = 1
    while y:
        if y % 2 != 0:
            res *= (x % p)
        y >>= 1
        x *= (x % p)
    return res

def C(n,m,p):
    if m > n:
        return 0
    a = (math.factorial(n)) % p
    b = (power(math.factorial(m), (p - 2))) % p
    c = (power(math.factorial(n - m), (p - 2))) % p
    return a*b*c % p

# Python Version
def Lucas(n, m, p):
  if m == 0:
      return 1
  return (Lucas(n // p, m // p, p) * C(n % p, m % p, p)) % p

def main():
    p = 2
    t = int(input())
    while t:
        t = t - 1
        n, m = map(int, input().split())
        print(Lucas(n, m, 2))
    pass
if __name__ == '__main__':
    main()

python的代码在平台上的运行时间比C++相比多了8倍,区别就在函数C的求解方法不同(原理相同都是费马小定理求乘法逆元)
在这里插入图片描述
中间超时的是最开始那个用高中组合数公式写的,严重超时


拓展

分享一篇很不错的文章:

组合数取模_ACdreamers的博客


⭐感谢您能看到这里,我会好好努力继续分享好的文章的!⭐

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值