题解 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;
}

 

posted @ 2022-03-27 19:48  Dzhao_qwq  阅读(38)  评论(0)    收藏  举报