P10112 [GESP202312 八级] 奖品分配

GESP8 冲刺课堂页:先建模,再压缩状态,最后读代码。

一、先回答问题

结论

奖品总数 S 只可能是 N 或 N+1,方案数就是多重集合排列 S! / ∏a_i!。

容易误入的路

按学生逐个分奖品会产生大量重复状态,也很难处理同类奖品不可区分。

二、核心观察

建模与压缩

把 N 个学生加上可能的一个“垃圾桶”看作 S 个位置,把同类奖品排列进去。

为什么不丢答案:每个合法分配唯一对应一个长度为 S 的多重集合排列,垃圾桶只吸收多出来的一个奖品。

S 阶乘 逆元

三、互动演示

步骤1
关键状态 A-
关键状态 B-
答案/目标-
复杂度看表
课堂动作手推

四、自己选答案

问题 1

S=N+1 时多出的奖品怎么处理?

问题 2

为什么要除以 a_i!?

五、手推结果

含义
S!所有奖品排到位置
a_i!同类奖品内部重复
逆元模意义下做除法

课堂要求:每题先说清对象、关系、保留的信息、压缩掉的信息,再写代码。

六、C++14 参考代码

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

typedef long long ll;
const int MOD = 1e9 + 7;
const int MAXS = 2005; // 虽然 N<=1000, 但为了保险开大一点

ll fac[MAXS], inv[MAXS];

// 快速幂求逆元
ll power(ll a, ll b) {
    ll res = 1;
    a %= MOD;
    while (b > 0) {
        if (b % 2 == 1) res = (res * a) % MOD;
        a = (a * a) % MOD;
        b /= 2;
    }
    return res;
}

ll get_inv(ll n) {
    return power(n, MOD - 2);
}

void precompute() {
    fac[0] = 1;
    for (int i = 1; i < MAXS; i++) {
        fac[i] = (fac[i - 1] * i) % MOD;
    }
}

void solve() {
    int n, m;
    if (!(cin >> n >> m)) return;
    ll s = 0;
    vector<int> a(m);
    for (int i = 0; i < m; i++) {
        cin >> a[i];
        s += a[i];
    }
    
    ll ans = fac[s];
    for (int i = 0; i < m; i++) {
        ans = (ans * get_inv(fac[a[i]])) % MOD;
    }
    cout << ans << "\n";
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    precompute();
    int t;
    cin >> t;
    while (t--) {
        solve();
    }
    
    return 0;
}