Acwing 组合计数

news2024/11/20 20:19:22

一个递推式:
从 a 个元素中选择 b 个,有多少种取法 C a b = a × ( a − 1 ) × ⋯ × ( a − b + 1 ) 1 × 2 × 3 × ⋯ × b = a ! b ! × ( a − b ) ! = C a − 1 b + C a − 1 b − 1 从a个元素中选择b个,有多少种取法C_{a}^{b} = \frac{a\times(a-1)\times\dots\times(a-b+1)}{1\times2\times3\times\dots\times b} \\=\frac{a!}{b!\times(a-b)!}\\= C_{a-1}^{b} + C_{a-1}^{b-1} a个元素中选择b个,有多少种取法Cab=1×2×3××ba×(a1)××(ab+1)=b!×(ab)!a!=Ca1b+Ca1b1
Acwing 855.求组合数 I
在这里插入图片描述
实现思路:询问10000次,数字范围2000,直接使用递推式

  • 递推式:
    C a b = C a − 1 b + C a − 1 b − 1 C_{a}^{b} = C_{a-1}^{b} + C_{a-1}^{b-1} Cab=Ca1b+Ca1b1
    其实就是动态规划DP, O ( n 2 ) . O(n^2). O(n2).
  • 因为数字范围就2000,直接先预处理出2000范围内的所有组合数,使用二维数组存储c[][],后续只需调用就行
  • 注意结果要取模,防止结果超出int范围

具体实现代码(详解版):

#include <iostream>

using namespace std;

const int N = 2010,mod = 1e9+7;
int c[N][N];

//预处理出这2000范围内的所有组合数
void init(){
    for(int i = 0 ; i < N ; i ++){
        for(int j = 0 ; j <= i ; j ++){
            if(!j) c[i][j] =1;
            else c[i][j] = (c[i-1][j] + c[i-1][j-1]) % mod;//递推式
        }
    }
}

int main(){
    int n;
    cin >> n;
    init();
    while(n --){
        int a,b;
        cin >> a >> b;
        cout << c[a][b] << endl;
    }
    return 0;
}

如果数据范围变大,递推的效率就会变低,超时!看下面的题目:
Acwing 886.求组合数II
在这里插入图片描述
实现思路:这是使用公式+乘法逆元的方法预处理得到阶乘
公式 C a b = a ! b ! × ( a − b ) ! 公式C_{a}^{b} =\frac{a!}{b!\times(a-b)!} 公式Cab=b!×(ab)!a!

  • 考虑到计算过程存在取模且公式存在分式,则需要将除法转化为乘法,因为除法取模会得到错误答案,所以转化为乘法再做取模运算,这时就想到乘法逆元(因为本题模数1e9+7是质数,所以可以使用快速幂求逆元,费马定理b^(m-2)。注意若没有对质数取模,则只能用扩展欧几里得算),对分母两个数分别取逆元再乘以分子,即可得到答案
  • 设置一个数组fact[],表示分子阶乘;数组infact[],表示分母阶乘的逆元,最后通过fact[a]*infact[b]*infact[a-b]得到组合数结果
    具体实现代码(详解版)
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL; 
const int N = 100010; 
const int mod = 1e9 + 7; // 模数 (10^9 + 7)

// 阶乘数组和阶乘逆元数组
LL fact[N], infact[N];

// 快速幂函数,计算 (a^k) % p
LL qmi(LL a, LL k, LL p) {
    LL res = 1; // 初始化结果为 1
    LL t = a % p; // 基数 a 取模

    // 进行快速幂计算
    while (k) {
        if (k & 1) // 如果当前位为 1
            res = (res * t) % p; // 更新结果
        t = (t * t) % p; // 基数平方并取模
        k >>= 1; // 右移 k,处理下一位
    }
    
    return res; // 返回结果
}

int main() {
    // 初始化阶乘和逆元
    fact[0] = infact[0] = 1; // 0! = 1
    for (int i = 1; i < N; i++) {
        fact[i] = (fact[i - 1] * i) % mod; // 计算阶乘
        infact[i] = (infact[i - 1] * qmi(i, mod - 2, mod)) % mod; // 计算阶乘的逆元
    }
    
    int n; 
    cin >> n; 
    while (n--) {
        int a, b; 
        cin >> a >> b; 
        
        // 计算并输出组合数 C(a, b)
        cout << (fact[a] * infact[b] % mod * infact[a - b] % mod) << endl;
    }
    
    return 0; 
}

如果数据范围继续增大,可以考虑使用Lucas定理
Acwing 887.求组合数III
在这里插入图片描述

  • 卢卡斯(lucas)定理:
  • C a b ≡ C a   m o d   p b   m o d   p × C a / p b / p ( m o d p ) C_{a}^{b} \equiv C_{a\ mod \ p}^{b\ mod \ p} \times C_{a/p}^{b/p} \pmod{p} CabCa mod pb mod p×Ca/pb/p(modp)
    lucas函数:函数的参数为a,b,若a<p且b<p则说明a,b足够小可以通过组合数函数C(a,b)直接计算,否则返回C(a%p,b%p)*lucas(a/p,b/p)(因为a,b除p可能依然很大,当足够小时就像上述情况直接返回组合数函数)
  • 注意这里求组合数C,与上题不同,无需枚举处理所有组合数,直接用基本公式:
    - C a b = a ∗ ( a − 1 ) . . . ∗ ( a − b + 1 ) b ! C_{a}^{b}=\frac{a*(a-1)...*(a-b+1)}{b!} Cab=b!a(a1)...(ab+1)
  • 采用类似双指针算法,指针i由后向前遍历到a-b+1,j由1向前遍历到b
  • 然后再利用乘法逆元(快速幂求逆元),将除法转化为乘法,取模

具体实现代码(详解版):

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL; // 定义长整型别名
const int N = 100010; // 最大支持的 n 值
const int mod = 1e9 + 7; // 模数

LL fact[N], infact[N]; // 分子阶乘数组,分母阶乘的逆元数组

// 快速幂算法,用于计算 (a^k) % p
LL qmi(LL a, LL k, LL p) {
    LL res = 1; // 结果初始值为 1
    LL t = a % p; // 基数 a 取模

    while (k) {
        if (k & 1) { // 如果 k 的当前位为 1
            res = (res * t) % p; // 更新结果
        }
        t = (t * t) % p; // 基数平方并取模
        k >>= 1; // 右移 k,处理下一位
    }
    
    return res; // 返回结果
}

// 基本公式求组合数,利用乘法逆元
int C(int a, int b, int p) {
    if (b < 0 || a < 0 || a < b) return 0; // 检查 b 是否负数,或者 a 是否小于 b
    LL x = 1, y = 1; // x 是分子,y 是分母
    for (int i = a, j = 1; j <= b; i--, j++) {
        x = (x * i) % p; // 计算分子
        y = (y * j) % p; // 计算分母
    }
    
    return x * (LL)qmi(y, p - 2, p) % p; // 返回组合数
}

// Lucas 定理计算组合数
int lucas(LL a, LL b, int p) {
    if (a < p && b < p) return C(a, b, p); // 基本情况
    // 递归计算
    return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}

int main() {
    int n; // 测试案例的数量
    cin >> n; // 输入测试案例数量
    while (n--) {
        LL a, b, p; // a 和 b 为组合数参数,p 为模数
        cin >> a >> b >> p; // 输入 a、b 和 p
        
        cout << lucas(a, b, p) << endl; // 输出结果
    }
    
    return 0;
}

当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用,同时需要用到高精度乘法。
Acwing 888.组合计数IV
在这里插入图片描述

实现思路:

  • 使用组合数公式
    C a b = a ! b ! × ( a − b ) ! C_{a}^{b} =\frac{a!}{b!\times(a-b)!} Cab=b!×(ab)!a!

  • 线筛法求出对于公式中最大的数a之前的所有的质数,之后在得到在a!的中所包含的每个质因子p对应的幂次
    a ! 中质因子 p 的幂次 = a p + a p 2 + . . . . . a p n ,向下取整 a!中质因子p的幂次=\frac{a}{p}+\frac{a}{p^2}+.....\frac{a}{p^n},向下取整 a!中质因子p的幂次=pa+p2a+.....pna,向下取整

  • 同理得到b!(a-b)!中对应p的质因子的幂次,然后a!中p的幂次-b!中p幂次-(a-b)!中p的幂次=结果中p的幂次,以此类推得到结果中每个质因子pi对应的幂次 最终结果 C a b = p 1 α 1 ∗ p 2 α 2 ∗ p 3 α 3 ∗ . . . . ∗ p k α k 最终结果C_{a}^{b}=p_{1}^{α1}*p_{2}^{α2}*p_{3}^{α3}*....*p_{k}^{αk} 最终结果Cab=p1α1p2α2p3α3....pkαk

  • 再对结果进行一个高精度的乘法得到最终结果

具体实现代码(详解版):

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 5010; 
int primes[N], cnt;
int sum[N]; // 存储每个质数的次数
bool st[N]; // 存储每个数是否被筛掉

// 线性筛法求素数
void get_primes(int n) {
    for (int i = 2; i <= n; i++) {
        if (!st[i]) primes[cnt++] = i; // 如果当前数为质数,则存储
        // 标记合成数
        for (int j = 0; primes[j] <= n / i; j++) {
            st[primes[j] * i] = true; // 标记 primes[j] * i 为合成数
            if (i % primes[j] == 0) break; // 防止重复标记
        }
    }
}

// 计算质因数的次数
int get(int n, int p) {
    int res = 0;
    while (n) {
        res += n / p; // 计算 n! 中 p 的次数
        n /= p; // 继续向下除以 p
    }
    return res;
}

// 高精度乘法
vector<int> mul(vector<int> a, int b) {
    vector<int> c; // 存储乘法结果
    int t = 0; // 进位
    for (int i = 0; i < a.size(); i++) {
        t += a[i] * b; // 逐位乘以 b
        c.push_back(t % 10); // 当前位
        t /= 10; // 更新进位
    }
    // 处理最后的进位
    while (t) {
        c.push_back(t % 10);
        t /= 10;
    }
    
    return c; 
}

int main() {
    int a, b;
    cin >> a >> b; 
    get_primes(a); // 预处理范围内的所有质数
    
    // 求每个质因数的次数
    for (int i = 0; i < cnt; i++) {
        int p = primes[i];
        sum[i] = get(a, p) - get(b, p) - get(a - b, p);
    }
    
    vector<int> res; // 存储结果
    res.push_back(1); // 初始化高精度数为 1
    
    // 用高精度乘法将所有质因子相乘
    for (int i = 0; i < cnt; i++) {//遍历所有质因子
        for (int j = 0; j < sum[i]; j++) {
            res = mul(res, primes[i]); // 对应幂次循环
        }
    }
    
    
    for (int i = res.size() - 1; i >= 0; i--) cout << res[i];
    
    return 0; 
}

用到了之前的高精度乘法、线性筛法求素数等,所以难题都是可以分解的,每部分都是模板题。
卡特兰数
Acwing 889.满足条件的01序列
在这里插入图片描述
思路分析:

  • 将01序列置于坐标系中,起点定于原点。若0表示向右走,1表示向上走,那么任何前缀中0的个数不少于1的个数就转化为:路径上的任意一点,横坐标大于等于纵坐标。题目所求即为这样的合法路径数量。
  • 下图中,表示从(0,0)到(n,n)的路径,在绿线即以下表示合法,若触碰红线则不合法。
    在这里插入图片描述

由图可知,任何一条不合法的路径(如黑色路径),都对应一条从(0,0)走到(n-1,n+1)的一条路径(如灰色路径).而任何一条(0,0)走到(n-1,n+1)的路径,也对应了一条从(0,0)走到(n,n)的不合法路径。

假设有6个0,6个1,设0表示向右走一步,1表示向上走一步,则路径是从原点(0,0)到点(6,6),需要12步。根据题目要求前缀中0的个数大于1的个数,转化为所走路径要满足的条件就是:路径上的每一点的坐标都必须满足横坐标x>=纵坐标y,即路径必须在图中红色斜线(y=x+1)的下方,不能与红色斜线有交叉。目标点(6,6)关于红色斜线对称的点为(5,7),则只要是从原点(0,0)走到(5,7)的路径必然会与红线有交叉,即不符合路径的要求。
(0,0)–> (6,6)的路径方案数:C_{12}^{6} (总方案)

(0,0)–> (5,7)的路径方案数:C_{12}^{5} (不符合条件的方案)
故符合条件的方案数C_{12}^{6} - C_{12}^{5}
由以上推广到 n 个 0 和 n 个 1 ,得到公式 : C 2 n n − C 2 n n − 1 = C 2 n n n + 1 = ( 2 n ) ! ( n + 1 ) ! n ! ,即卡特兰数 由以上推广到n个0和n个1,得到公式:\\C_{2n}^{n}-C_{2n}^{n-1}=\frac{C_{2n}^{n}}{n+1}=\frac{(2n)!}{(n+1)!n!},即卡特兰数 由以上推广到n0n1,得到公式:C2nnC2nn1=n+1C2nn=(n+1)!n!(2n)!,即卡特兰数
注:这里结果依旧要对一个质数取模,所以可以用费马定理,快速幂求逆元

具体实现代码(详解版):

#include <iostream>
#include <algorithm>

using namespace std;

const int mod = 1e9 + 7;
typedef long long LL; 

// 快速幂
LL qmi(int a, int k, int p) {
    int res = 1; // 初始化结果为1
    int t = a; // 初始化当前基数
    while (k) { // 当 k 不为0
        if (k & 1) res = (LL)res * t % p; // 当 k 的当前位为1,乘入当前基数
        t = (LL)t * t % p; // 当前基数平方
        k >>= 1; // 右移 k,准备下一轮
    }
    return res; 
}

// 用组合数基本公式求卡特兰数
LL Katelan(int n) {
    int a = 2 * n, b = n; // 2n 和 n
    LL res = 1; // 初始化结果
    for (int i = a, j = 1; j <= b; j++, i--) {
        res = (LL)res * i % mod; // 计算 (2n)! / n!
        res = (LL)res * qmi(j, mod - 2, mod) % mod; // 乘以 (n!) 的逆元
    }
    return (LL)res * qmi(n + 1, mod - 2, mod) % mod; // 乘以 (n+1) 的逆元
}

int main() {
    int n;
    cin >> n; 
    LL res = Katelan(n); 
    cout << res << endl; 
    return 0; // 程序结束
}

以上就是关于组合数的几种计算方法,我们要根据数据的大小范围确定使用哪种方法,对于最后的卡特兰数,就是组合数的一个应用。它是从一个01序列的问题推出来的,比较有技巧性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2182803.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基础算法之双指针--Java实现(上)--LeetCode题解:移动零-复写零-快乐数-盛最多的水

这里是Thembefue 今天讲解算法中较为经典的一个算法 本讲解主要通过题目来讲解以理解算法 讲解分为三部分&#xff1a;题目解析 > 算法讲解 > 编写代码 移动零 题目链接&#xff1a; 移动零 题目解析 这题的题目意思还是比较好读懂的 就是将数组出现零的地方移到数组最后…

【SpringCloud】 统⼀服务⼊⼝-Gateway

统⼀服务⼊⼝-Gateway 1. ⽹关介绍1.1 问题1.2 什么是API⽹关1.3 常⻅⽹关实现ZuulSpring Cloud Gateway 2. 上手 1. ⽹关介绍 1.1 问题 前⾯的课程中, 我们通过Eureka, Nacos解决了服务注册, 服务发现的问题, 使⽤Spring Cloud LoadBalance解决了负载均衡的问题, 使⽤OpenFe…

使用 Seaborn 热图的 5 种方法(Python 教程)

如何计算 SHAP 特征贡献的概述 原文地址: https://mp.weixin.qq.com/s/nBb9oKlSzRW8w7widHJr6w 热图可以让你的数据变得生动。用途广泛且引人注目。在很多情况下,它们可以突出显示数据中的重要关系。具体来说,我们将讨论如何使用它们来可视化: 模型准确度的混淆矩阵时间序列…

如何从硬盘恢复丢失/删除的视频

您是否想知道是否可以恢复已删除的视频&#xff1f; 幸运的是&#xff0c;您可以使用奇客数据恢复从硬盘驱动器、SD 卡和 USB 闪存驱动器恢复已删除的视频文件。 你有没有遇到过这样的情况&#xff1a;当你随机删除文件以释放空间时&#xff0c;你不小心按下了一些重要视频的…

SysML案例-停车场

DDD领域驱动设计批评文集>> 《软件方法》强化自测题集>> 《软件方法》各章合集>>

求职Leetcode题目(12)

1.只出现一次的数字 异或运算满足交换律 a⊕bb⊕a &#xff0c;即以上运算结果与 nums 的元素顺序无关。代码如下&#xff1a; class Solution {public int singleNumber(int[] nums) {int ans 0;for(int num:nums){ans^num;}return ans;} } 2.只出现一次的数字II 这是今天滴…

跳跃列表(Skip List)详解

什么是跳跃列表&#xff1f; 跳跃列表是一种概率性的数据结构&#xff0c;旨在提高链表的搜索、插入和删除效率。它通过在普通链表的基础上增加多个层次&#xff0c;以实现更快的访问速度。跳跃列表的设计灵感来源于跳跃图&#xff08;Skip Graph&#xff09;和多层索引的概念…

使用Materialize制作unity的贴图,Materialize的简单教程,Materialize学习日志

Materialize 官网下载地址&#xff1a;http://boundingboxsoftware.com/materialize/ github源码地址&#xff1a;https://github.com/BoundingBoxSoftware/Materialize 下载地址&#xff1a;http://boundingboxsoftware.com/materialize/getkey.php 下载后解压运行exe即可 …

带徒实训项目实战讲义分享:ApiFirst文档对比功能页面开发

亲爱的学员朋友&#xff0c;前面咱一起实现了入参列表对比的部分功能&#xff0c;本节在此基础上继续开发和重构代码&#xff0c;go&#xff01; 文章目录 已实现的功能实现API入参列表的增删对比合并参数列表杜绝内部变量暴露提取modifiedType枚举 已实现的功能 基于0.0.6和…

算术操作符/和*、while、for循环

上一次我们讲到float等浮点型的数据范围和数据类型长度&#xff0c;以及sizeof可以查看变量、表达式、数据类型的字节数即所占内存。 除法/和乘法* 我们继续用计算器这个例子来学习其他语法。先来看最初我们写成的代码&#xff1a; #include<stdio.h> int Add(int a, …

基于YOLOv4和DeepSORT的车牌识别与跟踪系统

1. 项目简介 本项目旨在开发一个基于深度学习的自动车牌识别&#xff08;Automatic License Plate Recognition, ALPR&#xff09;系统&#xff0c;以实现对车辆牌照的实时检测、识别和追踪。自动车牌识别技术广泛应用于智慧交通、停车管理、电子收费和执法监控等领域&#xf…

Golang | Leetcode Golang题解之第440题字典序的第K小数字

题目&#xff1a; 题解&#xff1a; func getSteps(cur, n int) (steps int) {first, last : cur, curfor first < n {steps min(last, n) - first 1first * 10last last*10 9}return }func findKthNumber(n, k int) int {cur : 1k--for k > 0 {steps : getSteps(cu…

c++11新特性-下

c11的线程库可以跨平台使用。 原子性操作库(atomic) 不需要对原子类型变量进行加锁解锁操作&#xff0c;线程能够对原子类型变量互斥的访问。 atmoic<T> t; // 声明一个类型为T的原子类型变量t 在C11中&#xff0c;原子类 型只能从其模板参数中进行构造&#xff0c;不…

【规控+slam】探索建图方案及代码分享

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 TODO:写完再整理 文章目录 系列文章目录前言背景建图描述SLAM定位+感知数据标记构建地图自动探索建图规划方法一:手动遥控探索建图算法步骤方法二:手动给定目标点探索建图算法原理方法三:f…

VMware虚拟机连接公网,和WindTerm

一、项目名称 vmware虚拟机连接公网和windterm 二、项目背景 需求1&#xff1a;windows物理机&#xff0c;安装了vmware虚拟机&#xff0c;需要访问公网资源&#xff0c;比如云服务商的yum仓库&#xff0c;国内镜像加速站的容器镜像&#xff0c;http/https资源。 需求2&#xf…

Hive数仓操作(八)

一、Hive中的分桶表 1. 分桶表的概念 分桶表是Hive中一种用于提升查询效率的表类型。分桶指的是根据指定列的哈希值将数据划分到不同的文件&#xff08;桶&#xff09;中。 2. 分桶表的原理 哈希分桶&#xff1a;根据分桶列计算哈希值&#xff0c;对哈希值取模&#xff0c;将…

【漏洞复现】JeecgBoot 积木报表 queryFieldBySql sql注入漏洞

》》》产品描述《《《 积木报表&#xff0c;是一款免费的企业级Web报表工具&#xff0c;像搭建积木一样在线设计报表!功能涵盖&#xff0c;数据报表、打印设计、图表报表、大屏设计等! 》》》漏洞描述《《《 JeecgBoot 积木报表 queryFieldBySq| 接口存在一个 SQL 注入漏洞&…

Web和UE5像素流送、通信教程

一、web端配置 首先打开Github地址&#xff1a;https://github.com/EpicGamesExt/PixelStreamingInfrastructure 找到自己虚幻引擎对应版本的项目并下载下来&#xff0c;我这里用的是5.3。 打开项目找到PixelStreamingInfrastructure-master > Frontend > implementat…

NodeJS下载、安装及环境配置教程,内容详实

文章目录 概述关于本文NodeJS介绍 安装步骤 概述 关于本文 本文讲解如何在Windows系统中安装NodeJS并配置相关环境。 NodeJS介绍 Node.js&#xff08;通常简称为Node&#xff09;是一个开源、跨平台的JavaScript运行时环境&#xff0c;它允许开发者在服务器端运行JavaScrip…

【PyTorch】图像分割

图像分割是什么 Image Segmentation 将图像每一个像素分类 图像分割分类 超像素分割&#xff1a;少量超像素代替大量像素&#xff0c;常用于图像预处理语义分割&#xff1a;逐像素分类&#xff0c;无法区分个体实例分割&#xff1a;对个体目标进行分割全景分割&#xff1a;…