成绩比较
题目链接:ybt金牌导航8-3-6 / luogu P3270
题目大意
有 n 个人,第一个是自己,然后每个人每科有分数,给你科目数量,科目最高分,你的排名(排名比你高的分数都比你大,排名比你低的分数都比你小)。
然后告诉了你你完胜了多少个人(即每一科都大于等于它),然后问你有多少种满足条件的分数情况。
只要有一个人有一科的分数不同,那这个分数情况就是不同的。
思路
我们考虑 DP,设 \(f_{i,j}\) 为当前看了前 \(i\) 科,暂时完胜 \(j\) 个人。
显然初始化 \(f_{0,n-1}=1\)。
然后考虑转移:(先把式子给出来)
\(f_{i,j}=\sum\limits_{l=j}^kf_{i-1,l}\binom{j}{l}\binom{n-r_i-j}{n-1-l}\sum\limits_{p=1}^{u_i}p^{n-r_i}(u_i-p)^{r_i-1}\)
我们可以把这个转移分成三部分分别解释:
\(f_{i-1,l}\):这个显然。
\(\binom{j}{l}\binom{n-r_i-j}{n-1-l}\):我们考虑从原本 \(l\) 个完胜的选出 \(j\) 个继续完胜,那就是 \(\binom{j}{l}\),然后你胜的次数就用了 \(j\),一共能胜的次数是 \(n-r_i\)(败的次数是 \(r_i-1\)),还有 \(n-r_i-j\) 次,随机分配给剩下的。
然后要注意的是部分分派给 \(l\) 个中没有被选到的,所以是 \(\binom{n-r_i-j}{n-1-l}\)。
\(\sum\limits_{p=1}^{u_i}p^{n-r_i}(u_i-p)^{r_i-1}\):这个就是你要给所有人分配分数(你前面只是分配了排名)
然后你就考虑枚举自己的分数,那我们已经知道它胜了 \(n-r_i\) 个人,败了 \(r_i-1\) 个人。
那其他人之间的胜负我们不用管,所以被胜的人可以选 \(1\sim p\)(记得可以相同),一共是 \(p\) 种,所以是 \(p^{n-r_i}\)。被败的人可以选 \(p+1\sim u_i\),一共是 \(u_i-p\),所以是 \((u_i-p)^{r_i-1}\)。
然后我们发现 \(u_i\) 很大。
然后我们考虑优化这一维,观察发现 \(n-r_i\) 和 \(r_i-1\) 在枚举 \(i\) 的时候已经固定。
那就相当于 \(\sum\limits_{i=1}^yx^k(y-x)^l\) 这种形式。
然后你会发现这两个分别是 \(k,l\) 项的多项式,然后乘在一起就是 \(k+l\) 项,在原式中 \(k=n-r_i,l=r_i-1\),所以 \(k+l=n-1\),那前缀和一下就是 \(n\) 项的多项式!
那多项式我们就可以直接用拉格朗日插值来求 \(u_i\) 时的值了。
(我写题的时候没细看用了 \(n+1\) 项的,不过都无所谓可以过)
然后就搞好了。
代码
#include<cstdio>
#define ll long long
#define mo 1000000007
using namespace std;
ll n, m, k, u[109], r[109], inv_mo1;
ll jc[109], inv[109], f[109][109];
ll y[109], pre[109], suf[109], lft;
ll ksm(ll x, ll y) {
x = (x % mo + mo) % mo;
ll re = 1;
while (y) {
if (y & 1) re = re * x % mo;
x = x * x % mo;
y >>= 1;
}
return re;
}
ll C(ll n, ll m) {
if (n < m) return 0;
if (m < 0) return 0;
return jc[n] * inv[m] % mo * inv[n - m] % mo;
}
ll query(ll k, ll n) {//拉格朗日插值
ll re = 0;
for (int i = 1; i <= k; i++) {
re = (re + y[i] * pre[i - 1] % mo * suf[i + 1] % mo * inv[i - 1] % mo * inv[k - i] % mo * (((k - i) & 1) ? inv_mo1 : 1) % mo) % mo;
}
return re;
}
int main() {
inv_mo1 = ksm(mo - 1, mo - 2);//预处理拉格朗日插值的逆元,这样就不用带log(组合数中也用到)
jc[0] = 1; for (int i = 1; i <= 108; i++) jc[i] = jc[i - 1] * i % mo;
inv[0] = inv[1] = 1; for (int i = 2; i <= 108; i++) inv[i] = inv[mo % i] * (mo - mo / i) % mo;
for (int i = 1; i <= 108; i++) inv[i] = inv[i - 1] * inv[i] % mo;
scanf("%lld %lld %lld", &n, &m, &k);
for (int i = 1; i <= m; i++) scanf("%lld", &u[i]);
for (int i = 1; i <= m; i++) scanf("%lld", &r[i]);
f[0][n - 1] = 1;//当前 i 科,j 个碾压
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n + 2; j++)//提前把拉格朗日插值要的东西能先求就先求,减小复杂度
y[j] = (y[j - 1] + ksm(j, n - r[i]) * ksm(u[i] - j, r[i] - 1) % mo) % mo;
pre[0] = 1; for (int j = 1; j <= n + 2; j++) pre[j] = pre[j - 1] * (u[i] - j + mo) % mo;
suf[n + 2 + 1] = 1; for (int j = n + 2; j >= 1; j--) suf[j] = suf[j + 1] * (u[i] - j + mo) % mo;
for (int j = k; j < n; j++) {
for (int l = j; l < n; l++) {//DP
lft = C(l, j) * C(n - 1 - l, n - r[i] - j) % mo;
f[i][j] = (f[i][j] + f[i - 1][l] * lft % mo * query(n + 2, u[i]) % mo) % mo;
}
}
}
printf("%lld", f[m][k]);
return 0;
}