「2020-2021 集训队作业」Gem Island 2(二项式反演,组合数递推)
复盘去年 \(\color{black}{\text{4}}\color{red}{\text{182_543_731}}\) 讲的题,彻底感受到了大佬的强大!!1
Description
给定 \(n\) 个位置,每个位置初始值为 \(1\) ,给定 \(m\) 为可操作数,每次操作等概率选择一个位置数值 \(+ 1\) ,给定 \(k\) ,求数值前 \(k\) 大的总和期望值。
弱化版: \(1\leq n,\ m \leq 500,\ k \leq n\) 。
这道题: \(1\leq n,\ m \leq 1.5 \cdot 10 ^ 7,\ k \leq n\) 。
Analysis
用 \(m\) 或者 \(k\) 作为切入口会因为可能性太多计不来数,所以我们要勇于把枚举的数值作为切入口!!1
提前声明,根据题目有:
我们都知道有这么一个东西:
所以我们考虑计数,可以先想办法求这样一个东西:设 \(F_{a, i}\) 表示有 \(i\) 个位置里面数值大于 \(a\) 的方案数。
转化一下说法,恰好 \(i\) 个位置,一眼二项式反演,设 \(G_{a, i}\) 表示钦定了 \(i\) 个位置里面数值大于等于 \(a\) 的方案数。
\(G\) 是一个比较好求的东西,直接考虑意义,先从 \(n\) 个位置选择 \(i\) 个位置,其次利用插板法,题目要求 \(a \geq 1\) 的,所以我们从总数量中先剔出 \((i - 1) \cdot a\) ,然后在剩下的数里面放入 \(n - 1\) 个插板:
反演的两个式子都是最基本的:
然后对于每一个 \(a\) 都有贡献:
取较小值是题意,而对于每种 \(a\) ,考虑到原先那个式子,每一个小于等于 \(a\) 的状态都能贡献一个 \(1\) ,总和就是 \(a\) 。
这玩意硬要做是能做到 \(O(n ^ 2)\) 的,不过不能通过此题,所以必须继续推。
Solution
分析就放 \(O(n ^ 2)\) 做法属实有点尴尬。
我们能明显感觉到二项式反演已经帮了我们很大的忙,但是终究不够简洁,考虑暂时放弃。
组合意义灭天地,代数推导保平安。 ————tiger0133
我们考虑直接对 \(G\) 下手,直接利用反演的式子代换 \(F\) 。
更换一下 \(G\) 枚举的东西。
因为 \(G\) 是可以秒算的,所以我们考虑只去计算后面的系数。
因为形式比较复杂,我们不太可能从组合意义的角度分析,所以有可能需要递推计算。
我们令:
发现并没有什么比较直接的方法能够化简,不过我们都知道一个很经典的式子 \(\binom{n}{m} = \binom{n - 1}{m - 1} + \binom{n - 1}{m}\) ,来试试!
然后我们需要把这些转换成类似原先定义式的形式:
很明显前面两截已经吻合定义式了,同时后面这个可以视作组合数的一个公式:
所以就有:
显然根据定义式所有 \(n=0\) 的地方值都是 \(0\) ,可以忽略不计,所以就有:
边界: \(v_{1, k} = 1\) ;递推式: \(v_{n, k} = -v_{n - 1, k} + v_{n - 1, k - 1} (n > 1)\)
够简洁,但是不够直接,因为这似乎对我们的实际计算并没有什么太大的用处。
考虑组合意义,说说我自己的想法,我们可以把 \(n\) 看作集合内的元素总数, \(k\) 为选择的数量。
当两者同时减一时符号不变,反之变号,可以看作考虑剩下元素的奇偶性,添加 \((-1)\) 调整,所以有:
欸,又得考虑递推了,不过还是老办法:
显然后半截是可能出现负数下标的,更换一下,整体左移:
然后你发现发部分东西都被消掉了:
现在就是这样了:
边界: \(v_{1, k} = 1\) ;公式: \(v_{n, k} = (-1) ^ {n - k} \binom{n - 2}{k - 1} (n > 1)\)
所以说现在我们再来考虑答案:
要注意一下这里的 \(k\) 应当是 \(\min(i, k)\) 。因为本身 \(i\) 就是作为答案出现,所以不能超过 \(k\) 。
前面两截都可以快速算得,但是最后一个比较麻烦。因为好像可以看到类似因数求和得形式,可以考虑更换枚举顺序。
再换一下:
现在我们可以对于每一个 \(i\) 对最后一坨提前预处理。
明显地看到这是一个类似埃氏筛的东西,处理质数可以做到 \(O(n\log\log n)\) 处理,剩下的计算都是 \(O(n)\) 的,就能过题了。
最后的最后,我们只计算了方案数的答案贡献,所以要除掉总方案数 \(\binom{n + m - 1}{n - 1}\) 。
Code
Code
/*
*/
#include
using namespace std;
#define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout);
#define Check(a) freopen(a".in", "r", stdin), freopen(a".ans", "w", stdout);
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair pii;
#define fi first
#define se second
#define mp std::make_pair
const int mod = 998244353;
template
inline int M(A x) {return x;}
template
inline int M(A x, B ... args) {return 1ll * x * M(args...) % mod;}
#define mi(x) (x >= mod) && (x -= mod)
#define ad(x) (x < 0) && (x += mod)
const int N = 1.5e7 + 7, P = 1e6;
int n, m, k, fac[N << 1], inv[N << 1];
inline int ksm(int a, int b) {
int tm = 1;
for (; b; b >>= 1, a = M(a, a)) b & 1 && (tm = M(tm, a));
return tm;
}
inline int C(int n, int m) {return n < m || m < 0 ? 0 : M(fac[n], inv[m], inv[n - m]);}
int pri[P], tot, xs[N];
bool vi[N];
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cin >> n >> m >> k;
fac[0] = inv[0] = 1;
for (int i = 1; i <= n + m; ++i) fac[i] = M(fac[i - 1], i);
inv[n + m] = ksm(fac[n + m], mod - 2);
for (int i = n + m - 1; i; --i) inv[i] = M(inv[i + 1], i + 1);
xs[1] = C(n + m - 2, n - 1);
for (int i = 2; i <= m; ++i) {
!vi[i] && (pri[++tot] = i);
for (int j = 1; j <= tot && pri[j] * i <= m; ++j) {
vi[i * pri[j]] = 1;
if (!(i % pri[j])) break;
}
xs[i] = C(n + m - i - 1, n - 1);
}
for (int j = 1; j <= tot; ++j) {
for (int i = m / pri[j]; i >= 1; --i) xs[i] += xs[i * pri[j]], mi(xs[i]);
}
int cb = C(n + m - 1, n - 1), ans = M(n, xs[1] + cb);
for (int i = 2; i <= n; ++i) {
int nu = std::min(i, k), ret = M(C(n, i), xs[i] + cb, C(i - 2, nu - 1));
if ((i ^ nu) & 1) ans -= ret, ad(ans); else ans += ret, mi(ans);
}
std::cout << M(ksm(cb, mod - 2), ans) << "\n";
return 0;
}