The 2021 CCPC Weihai Onsite - M. 810975 题解

题意

Zayin进行了 n 场比赛, 获胜 m 局,其中最长连胜是 k ,问有多少种情况

思路

问题转化

考虑败场分割胜场(这是一个经典解法:隔板法),问题转化为

i=1nm+1xi=m (xik)

的非负整数解个数(整数拆分问题)。

G(x)=(i=0kxi)nm+1=(1+x+x2+x3++xk)nm+1=(11xxk+11x)nm+1

TIP: 如果你打算跑NTT快速幂 / 生成函数展开,看到这里就结束了。

引理

i=1pxi=m

的非负整数解个数为

C(m+p1, p1)=Cm+p1p1

引理证明

先考虑正整数解个数

考虑有 m 个球,分为 p 组,那么只需要这么考虑

O _ O _ O _ O _ O _ ....... _ O _ O // O 表示球, _ 表示空位

那么往空位塞入 p1 个隔板即可,C(m1, p1)

再考虑非负整数解个数

yi=xi+1

也就是考虑:先给每组球多塞一个球,作上述隔板法,就可以扩展到非负整数了

i=1pxi=  mi=1p(xi+1)=  m+pi=1pyi=  m+p

因为第三个式子正整数解个数即为 C(m+p1, p1),也等于第一个式子的非负整数解个数

至此,我们得到了 n 场比赛,获胜 m 场的情况数

回到原问题

以和引理一样的思路,我们考虑某一场比赛 至少 赢了 k+1 次(因为组合的性质,我们不好解决恰好 k 次的问题),减去至少赢了 k 次就是答案了。

为了防止问题太过抽象,我们考虑 n=7,m=6,k=3,p=nm+1=2 的情况,显然只有一种

首先,我们先抽一个 k 出来,算 i=1pyi=y1+y2=mk=3 情况数,然后 C(p,1) 塞给某1个 yi ,即可保证得到的组合至少存在1个大于 k 的数。通过引理我们知道答案是 C(3+21,21)C(2,1)=C(4,1)2=8

同理,k+1也是这么算,得到答案 6 ,但是 86=21

通过式子所代表的含义,我们列举出生成的 8k 的组合和 6k+1 的组合。(如果读者在这里没发现这里需要容斥,还请模拟一下8+6种情况,不会花太多时间)

发现不对,y1=y2=3 被计入了 2 次。显然,这是因为+3计数重复。本质上,我们要求的是 y[1] >= 3 or y[2] >= 3 (记作 A ) ,但是我们这里算的是的是y[1] >= 3 + y[2] >= 3 (记作 B ),要想不重复,我们只能减去y[1] >= 3 and y[2] >= 3(记作 C )。即 A=BC,同理,我们可以推广到 p > 2 的情况,即计算

Ans|k=i=1(1)iC(nm+1,i)C(mik+nm,nm)

i 表示枚举有 i 个元素 k,左边那个组合数表示选 i 个数 +k ,右边那个组合数即为整数拆分非负整数解个数

k+1 同理。

最后

Answer=Ans|kAns|k+1

小细节

具体看代码特判

代码

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;

const i64 MOD = 998244353;
const int N = 1e5 + 6;
i64 fac[N], inv[N], ifac[N];

void norm(i64 &x) {
    while(x>=MOD) x -= MOD;
    while(x<0) x += MOD;
}

i64 C(i64 n,i64 m) {
    if(m<0 || n<m) return 0;
    return fac[n] * ifac[m] % MOD * ifac[n-m] % MOD;
}

int main(int argc, char const *argv[])
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr); cout.tie(nullptr);

    // freopen("in.txt","r",stdin);

    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    ifac[0] = ifac[1] = 1;

    for(int i=2;i<N;++i) {
        fac[i] = fac[i-1] * i % MOD;
        inv[i] = inv[MOD%i] * (MOD-MOD/i) % MOD;
        ifac[i] = ifac[i-1] * inv[i] % MOD;
    }

    i64 n,m,k;
    cin >> n >> m >> k;

    if(!(n>=m && m>=k)) {
        cout << 0;
        return 0;
    }
    
    if(k==0) {
        if(m==0) cout << 1;
        else cout << 0;
        return 0;
    }

    vector<i64> a(m+2), b(m+2);
    i64 w = m, f = n - m;

    i64 ans = 0;
    for(int i=1;i*k<=m;++i) {
        // k
        a[i] = C(f + 1, i) * C(m - i * k + f, f) % MOD;
        // k + 1
        b[i] = C(f + 1, i) * C(m - i * (k + 1) + f, f) % MOD;

        ans += (i&1) ? (a[i] - b[i]) : (b[i] - a[i]);
        norm(ans);
    }

    cout << ans;

    return 0;
}
posted @   LacLic  阅读(877)  评论(4编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示