P2822 [NOIP2016 提高组] 组合数问题

P2822 [NOIP2016 提高组] 组合数问题

题解

作者 岛田小雅

这是一道复杂度非常容易爆炸的问题。

我看到这题的第一眼,第一反应是直接按照公式暴力跑。我们看一眼数据范围。如果直接算,复杂度是 \(O(t(nm)^2)+X>2\times{10^{10}}\)。肯定不能用公式。

既然数据那么大,我们应该想到预处理一些数据,让每个测试点都能用,从而避免一些重复的运算。那其实很容易就能想到可以把所有的组合数都算出来然后存起来。但是 \(n\) 的最大值是 \(2000\),如果直接存的话肯定存不了那么大的数。怎么办呢?其实我们可以对它进行取模,储存取模的结果。学过乘法逆元的小伙伴(比如我)这个时候眼睛一亮:哎我们是不是还要算这玩意对 \(k\) 的逆元?

大可不必,直接让它对 \(k\) 取模就行了。这样一来能被 \(k\) 整除的组合数存储结果是 \(0\),不能被整除的,结果不是 \(0\),方便我们进行计数(还有个原因是我已经把怎么算乘法逆元忘干净了)

存储的问题解决了,那么预处理的时候我们需要用题目给的公式计算吗?要知道计算阶乘是一个很庞大的工程,虽然我不知道它的具体复杂度是多少,但肯定会严重拖慢程序的速度。怎么办呢?学过高中数学的小伙伴一定知道,把组合数叠在一起可以叠出一个杨辉三角。也就是说我们用加法就可以计算出所有的组合数。

看到这里,我们已经能把大部分预处理写完了。然而还有一处需要优化,就是对答案的计数。我们需要数的是杨辉三角内一个四边形(当 \(m\leqslant{i}\) 时是个三角形)里面 \(0\) 的个数。比起每次一个一个计算,用前缀和进行预处理显然快速很多。

具体细节详见 AC 代码部分。

AC 代码

作者 岛田小雅
#include <bits/stdc++.h>
using namespace std;

const int N = 2e3+2;
int k, t;
int C[N][N], qzh[N][N];

void generate_C(int n, int m)
{
    if(!m || m==n) C[n][m] = 1;
    else C[n][m] = (C[n-1][m-1]+C[n-1][m])%k;
}

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    cin >> t >> k;
    for(int n = 1; n <= 2000; n++)
    {
        for(int m = 0; m <= n; m++)
        {
            generate_C(n,m);
            if(!m) qzh[n][m] = !C[n][m];
            else qzh[n][m] = qzh[n][m-1]+!C[n][m];
        }
    }
    while(t--)
    {
        int n, m;
        cin >> n >> m;
        int ans = 0;
        for(int i = 1; i <= n; i++) ans += qzh[i][min(i,m)];
        cout << ans << '\n';
    }
    return 0;
}
posted @ 2022-09-23 00:38  岛田小雅  阅读(33)  评论(0编辑  收藏  举报