LOJ3405 「2020-2021 集训队作业」Gem Island 2

LOJ 传送门

组合计数神题。下文的 m 指原题面中的 dk 指原题面中的 r

考虑最后每个人得到的宝石数量的序列 s1,s2,,sn,考虑这种方案的出现次数。首先要在 m 次操作中分别选 si1 次给第 i 个人,方案数为 m!i=1n(si1)!;然后对于一个人,当它的宝石数为 k 时,选择一颗变成两颗的方案数为 k,所以每个人还有 (si1)! 种方案的贡献,乘起来发现 (si1)! 被约掉了,也就是说对于任意一种 s1,s2,,sn,它们出现概率均等。

于是题目变成了,一个序列的价值为前 k 大元素之和,求所有满足 i=1nsi=m 的序列 s 的价值之和(最后要除个方案数 (n+m1n1) 和加上原有的 k 个)。

考虑对于一个固定的序列 s,我们以这样的方式计算它的价值:a>0min(k,i=1n[sia])。可以想象成一个柱状图,就是把原来的从左到右算面积变成从上到下算面积。这样可以方便许多。

那么设 fa,i 为对于一个序列 s恰好i 个元素的值 a 的这样的序列个数;考虑二项式反演,设 ga,i 为对于一个序列 s钦定i 个元素的值 a 的这样的序列个数。ga,i 是好求的,相当于选 i 个位置让他们 a,就变成了一个和为 mai 的插板,那么:

ga,i=(ni)(m+nai1n1)

且:

ga,i=j=in(ji)fa,j

反演得:

fa,i=j=in(1)ji(ji)ga,j

答案即为:

a=1mi=1nfa,i×min(i,k)

这样已经可以做 O(n2m) 了,但是还不够。考虑把 f 扔掉,直接计算每个 ga,i 对答案的贡献,那么答案也可以表示成:

a=1mi=1nga,ij=1i(1)ij(ij)min(j,k)

后面那一坨求和式子看上去不能考虑组合意义,那么可以尝试递推。设 vn,k=i=1n(1)ni(ni)min(i,k),那么根据 (nm)=(n1m)+(n1m1) 可以裂项后尝试递推:

vn,k=i=1n(1)ni(ni)min(i,k)=i=1n(1)ni(n1i)min(i,k)+i=1n(1)ni(n1i1)min(i,k)=i=1n1(1)n1i(n1i)min(i,k)+i=1n(1)n1(i1)(n1i1)(min(i1,k1)+1)=i=1n1(1)n1i(n1i)min(i,k)+i=1n1(1)n1i(n1i)min(i,k1)+i=1n1(1)n1i(n1i)=vn1,k+vn1,k1+(1+1)n11=vn1,k+vn1,k1+[n=1]

那么我们得到了 vn,k=vn1,k+vn1,k1,边界 k>0,v1,k=1。考虑组合意义,可以看作是,n1 次选 i 次让 k1n1i 次让 k1 并变号,那么有 vn,k=i=0k1(1)n1i(n1i)。再尝试递推:

vn,k=i=0k1(1)n1i(n1i)=i=0k1(1)n1i(n2i)+i=0k1(1)n1i(n2i1)=i=0k1(1)ni(n2i)+i=0k2(1)ni(n2i)=(1)nk(n2k1)

最后一步是运用了正负项抵消。那么我们有 k>0,v1,k=1,n>1,vn,k=(1)nk(n2k1)

再来考虑答案的式子:

a=1mi=1nga,ivi,k

分别代入 ga,i,vi,k 的式子得:

na=1m(m+na1n1)+a=1mi=2n(ni)(m+nai1n1)(1)ik(i2k1)

关注后半部分:

a=1mi=2n(ni)(m+nai1n1)(1)ik(i2k1)

注意到 aim,所以暴力算的复杂度就是 O(mlogm) 的。但是还能更优。

ans=a=1mi=2n(ni)(m+nai1n1)(1)ik(i2k1)=t=2m(m+nt1n1)it(ni)(1)ik(i2k1)=i=2n(ni)(1)ik(i2k1)it(m+nt1n1)=i=2n(ni)(1)ik(i2k1)hi

其中 hi=it(m+nt1n1),可以高维前缀和预处理出。

至此,我们终于以 O(n+mloglogm) 的复杂度完成了这题。

code
// Problem: #3405. 「2020-2021 集训队作业」Gem Island 2
// Contest: LibreOJ
// URL: https://loj.ac/p/3405
// Memory Limit: 512 MB
// Time Limit: 1200 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
#define pb emplace_back
#define fst first
#define scd second
#define mkp make_pair
#define mems(a, x) memset((a), (x), sizeof(a))
using namespace std;
typedef long long ll;
typedef double db;
typedef unsigned long long ull;
typedef long double ldb;
typedef pair<ll, ll> pii;
const int maxn = 30000100;
const ll mod = 998244353;
inline ll qpow(ll b, ll p) {
ll res = 1;
while (p) {
if (p & 1) {
res = res * b % mod;
}
b = b * b % mod;
p >>= 1;
}
return res;
}
int n, m, K, N, fac[maxn], ifac[maxn], f[maxn], pr[maxn / 10], tot;
bool vis[maxn];
inline ll C(int n, int m) {
if (n < m || n < 0 || m < 0) {
return 0;
} else {
return 1LL * fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}
}
void solve() {
scanf("%d%d%d", &n, &m, &K);
N = n + m;
fac[0] = 1;
for (int i = 1; i <= N; ++i) {
fac[i] = 1LL * fac[i - 1] * i % mod;
}
ifac[N] = qpow(fac[N], mod - 2);
for (int i = N - 1; ~i; --i) {
ifac[i] = 1LL * ifac[i + 1] * (i + 1) % mod;
}
for (int i = 2; i <= m; ++i) {
if (!vis[i]) {
pr[++tot] = i;
}
for (int j = 1; j <= tot && i * pr[j] <= m; ++j) {
vis[i * pr[j]] = 1;
if (i % pr[j] == 0) {
break;
}
}
}
for (int i = 1; i <= m; ++i) {
f[i] = C(n + m - i - 1, n - 1);
}
for (int i = 1; i <= tot; ++i) {
for (int j = m / pr[i], k = m / pr[i] * pr[i]; j; --j, k -= pr[i]) {
int &t = f[j];
t = (((t += f[k]) >= mod) ? t - mod : t);
}
}
ll ans = 1LL * n * f[1] % mod;
for (int i = 2; i <= n; ++i) {
ans = (ans + C(n, i) * (((i - K) & 1) ? mod - 1 : 1) % mod * C(i - 2, K - 1) % mod * f[i]) % mod;
}
ans = ans * qpow(C(n + m - 1, n - 1), mod - 2) % mod;
ans = (ans + K) % mod;
printf("%lld\n", ans);
}
int main() {
int T = 1;
// scanf("%d", &T);
while (T--) {
solve();
}
return 0;
}
posted @   zltzlt  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示