题解 CF958F3 Lightsabers (hard)
题意:给定一个可重集合,其中有 $n$ 个元素,元素范围为 $[1, m]$,
求它大小为 $k$ 的子集有多少种情况。
题解:每种元素的选法都可以用一个生成函数表示 $G_i(x)=\sum\limits_{j=0}^{cnt_i}x^j$ 分别表示选 $0$ 个到 $cnt_i$ 个,$cnt_i$ 表示有多少种 $i$ 元素。
那么答案就是把这 $m$ 个生成函数卷起来后 $x^k$ 项的系数,由于如果一项一项卷起来时间复杂度会退化为 $\mathcal{O}(n^2\log n)$。考虑优化,我们可以分治,定义 $solve(l, r)$ 为 $[l, r]$ 卷起来得到的多项式,那么可以在做 $[l, r]$ 时先做 $[l, mid]$ 和 $[mid+1, r]$ 然后把两边卷起来,这样的复杂度是 $\mathcal{O}(n\log^2 n)$ 的,因为总共只会递归 $\log n$ 层,每个多项式只会被 DFT $\log n$ 次,而多项式总次数又是 $\mathcal{O}(n)$ 的,所以总时间复杂度为 $\mathcal{O}(n\log^2 n)$。
注意:由于模数不是 NTT 模数,所以要用 FFT,注意需要开 long long,因为卷起来系数可能会很大。
//省去了又臭又长的头 using Poly_ftt :: mul; using Poly_ftt :: mul_ll; poly operator * (poly a, poly b) {return mul(a, b);} poly_ll operator * (poly_ll a, poly_ll b) {return mul_ll(a, b);} const int N = 2e5 + 5; int n, m, K, cnt[N]; auto solve(int l, int r) { if (l == r) { poly_ll ret(cnt[l] + 1); for (int i = 0; i <= cnt[l]; i++) ret[i] = 1; return ret; } int mid = l + r >> 1; poly_ll ret = solve(l, mid) * solve(mid + 1, r); for (int i = 0; i < (int)ret.size(); i++) ret[i] %= mod; return ret; } int main() { scanf("%d%d%d", &n, &m, &K); for (int i = 1, x; i <= n; i++) scanf("%d", &x), cnt[x]++; poly_ll res = solve(1, m); printf("%lld\n", res[K]); return 0; }

浙公网安备 33010602011771号