【计蒜客题解/洛谷题解/NOIP2016提高组】T2034/P2822 组合数问题

本题目的题解写得较长,照顾一些初中同学(虽然我自己也是初中生),同时也方便作者以后复习使用。如有错误或疑问,请尽快私信作者

题目概况

洛谷链接: https://www.luogu.com.cn/problem/P2822
计蒜客链接: https://nanti.jisuanke.com/t/T2034
**难度:**普及+/提高(计蒜客评级普及T3 ,是不是低了

题目分析

简化题目: 定义一个数组求出C[n][min(n, m)]中满足C(m, n)k的倍数的有多少
涉及知识点: 前缀和以及杨辉三角,组合数的相关公式等数学预备知识
解题思路:
在讲思路之前,我先简单说一下预备知识供大家参考:

一、组合数

在这里插入图片描述
数据范围较大,阶乘计算不现实,在这里给出一个关于组合数的递推公式(本题只涉及 例1,例 2各位看官有兴趣可以了解一下)
对,就是高中数学选修二里的那玩意(翻教材去咯~)
在这里插入图片描述
我们也可以把该式变形为:C(m, n) = C(m, n - 1) + C(m - 1,n - 1)
所以用代码实现,递推式即为c[i][j] = c[i - 1][j] + c[i - 1][j - 1]

二、杨辉三角在这里插入图片描述

形如此图即为杨辉三角,应该都听过,递推式即为dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1]

三、解题思路概括

首先我们发现组合数递推公式和杨辉三角递推公式相同,也就是说如果抽象的组合数我们无法想明白,就把它理解为杨辉三角做一个前缀和的预处理,但要注意,在最朴素的杨辉三角中,最右侧的元素无法更新,我们需要通过继承的方式从上到上更新。这个组合数我们可以在过程中直接模k,如果模k等于0,就把数量在前缀和中加1。

代码要点拆解

一、预处理环节

直接看注释
二维前缀和递推公式:
在这里插入图片描述
注意区分 :区域是前缀和的区域; 子矩阵指的是原矩阵中的区域
令x2 = n, y2 = m,假设A区域等于A子矩阵(视同)就是(1,1)。我们要输出的是D区域的,但是:

                  D = C区域(A,C子矩阵) + B区域(A,B子矩阵)                

所以在加上B,C区域后,我们需要把A区域再减一遍,去重。

    //预处理
    c[0][0] = c[1][0] = c[1][1] = 1; //三角上第0行和第1行需要预定义(类比组合数)
    for (int i = 2; i <= 2000; i++) {
        c[i][0] = 1; //杨辉三角的每行第一列都是1,也就是C(0, i) = 1
        for (int j = 1; j <= i; j++) {
            c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % k; //组合数递推
            ans[i][j] = ans[i][j - 1] + ans[i - 1][j] - ans[i - 1][j - 1]; //二维前缀和递推
            if (c[i][j] == 0) { //如果这个组合数模K后得到的余数是0
                ans[i][j]++;
            }
        }
        ans[i][i + 1] = ans[i][i]; //最右侧的继承,直接把上一行的最右侧搬下来更新
    }

二、输出环节

输出格式里有,m有可能大于n,所以我们需要min(n, m)

   while (t--) {
        scanf("%d%d", &n, &m);
        printf("%lld\n", ans[n][min(n, m)]);
    }

完整代码

#include <iostream>
#include <cstdio>
#define ll long long int
using namespace std;

const int MAXN = 2005;
int n, m, t, k;  //见题面
ll ans[MAXN][MAXN], c[MAXN][MAXN]; //答案数组和组合数存储数组

int main() {
    //freopen("problem.in", "r", stdin);
    //freopen("problem.out", "w", stdout); 
    scanf("%d%d", &t, &k);
    //预处理
    c[0][0] = c[1][0] = c[1][1] = 1;
    for (int i = 2; i <= 2000; i++) {
        c[i][0] = 1;
        for (int j = 1; j <= i; j++) {
            c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % k; //组合数递推
            ans[i][j] = ans[i][j - 1] + ans[i - 1][j] - ans[i - 1][j - 1]; //前缀和
            if (c[i][j] == 0) {
                ans[i][j]++;
            }
        }
        ans[i][i + 1] = ans[i][i];
    }
    while (t--) {
        scanf("%d%d", &n, &m);
        printf("%lld\n", ans[n][min(n, m)]);
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oier_Asad.Chen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值