[雅礼集训 2018 Day4]Magic

一、题目

点此看题

二、解法

容斥是不难想到的,我的主要瓶颈在于算钦定有 x x x对魔术对的方案数。这里有两种思路,第一种是安排法(都是蒟蒻作者乱取的名字),也就是我们通过直接安排顺序来算方案数,第二种是插入法,也就是我们通过插入的方式使之满足条件,本题第一种方法应该是走不通的,让我们来康康第二种方法。

我们先把 n − x n-x nx张牌随便乱放(插入不是在长度为 n n n的格子上填数,你可以理解为动态数组),所以这样的方案数是 ( n − x ) ! (n-x)! (nx)!,但是还要考虑相同的之间的顺序就很麻烦,我们干脆把相同的牌也看做不同,这样最后除以 ∏ a i ! \prod a_i! ai!就可以了。然后设 f ( i , j ) f(i,j) f(i,j)为考虑前 i i i种颜色,选了 j j j个魔术对的方案数,我们考虑转移。

其实算 f f f的过程相当于一个背包,考虑第 i i i种颜色贡献 b i b_i bi个魔术对( b i < a i b_i<a_i bi<ai)的方案数,第一张牌有 a i − b i a_i-b_i aibi个位置可以插入,第二张牌有 a i − b i + 1 a_i-b_i+1 aibi+1个位置可以插入 . . . . . . . ....... .......那么方案数就是 ( a i − 1 ) ! ( a i − b i − 1 ) ! \frac{(a_i-1)!}{(a_i-b_i-1)!} (aibi1)!(ai1)!,由于相同的牌不同,我们先要从 a i a_i ai中选出 b i b_i bi个构成魔术对的牌,方案数 C ( a i , b i ) C(a_i,b_i) C(ai,bi),把这两个乘起来就是背包的转移系数了。

外面套一个容斥: ∑ i = k n ( − 1 ) i − k × C ( i , k ) × f ( m , i ) × ( n − i ) ! \sum_{i=k}^n (-1)^{i-k}\times C(i,k)\times f(m,i)\times (n-i)! i=kn(1)ik×C(i,k)×f(m,i)×(ni)!,这就是二项式反演的常用形式,不懂的可以康康这篇博客。这样做的复杂度是 O ( n m ) O(nm) O(nm)的,发现背包的过程其实是卷积的过程,我们每次把最小的两项卷起来就行了,相当于一个启发式背包,这样做的时间复杂度是 O ( m log ⁡ 2 n ) O(m\log^2 n) O(mlog2n)的。

作者并不会证明时间复杂度(找时间问问 j z m jzm jzm也许?),贴个代码。

#include <cstdio>
#include <vector>
#include <iostream>
#include <queue>
using namespace std;
const int N = 20005;
const int M = 100005;
const int MOD = 998244353;
#define int long long
int read()
{
    int num=0,flag=1;
    char c;
    while((c=getchar())<'0'||c>'9')if(c=='-')flag=-1;
    while(c>='0'&&c<='9')num=(num<<3)+(num<<1)+(c^48),c=getchar();
    return num*flag;
}
int n,m,k,ans,pr,lg,len,fac[M],inv[M],A[M*2],B[2*M],Rev[2*M];
vector<int> f[N];
struct node
{
    int x,s;
    bool operator < (const node &b) const
    {
        return s>b.s;
    }
};
priority_queue<node> q;
void init(int n)
{
    fac[0]=inv[0]=inv[1]=1;
    for(int i=2; i<=n; i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
    for(int i=1; i<=n; i++) inv[i]=inv[i-1]*inv[i]%MOD;
    for(int i=1; i<=n; i++) fac[i]=fac[i-1]*i%MOD;
}
int C(int n,int m)
{
    return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
int qkpow(int a,int b)
{
    int r=1;
    while(b>0)
    {
        if(b&1) r=r*a%MOD;
        a=a*a%MOD;
        b>>=1;
    }
    return r;
}
void NTT(int *a,const int len,int tmp)
{
    for(int i=0; i<len; i++)
    {
        Rev[i]=(Rev[i>>1]>>1)|((i&1)<<(lg-1));
        if(i<Rev[i])
            swap(a[i],a[Rev[i]]);
    }
    for(int s=2; s<=len; s<<=1)
    {
        int t=s/2,w=(tmp==1)?qkpow(3,(MOD-1)/s):qkpow(3,(MOD-1)-(MOD-1)/s);
        for(int i=0; i<len; i+=s)
        {
            int x=1;
            for(int j=0; j<t; j++,x=x*w%MOD)
            {
                int fe=a[i+j],fo=a[i+j+t];
                a[i+j]=(fe+x*fo)%MOD;
                a[i+j+t]=((fe-fo*x)%MOD+MOD)%MOD;
            }
        }
    }
    if(tmp==1) return ;
    int inv=qkpow(len,MOD-2);
    for(int i=0; i<len; i++)
        a[i]=a[i]*inv%MOD;
}
signed main()
{
    init(1e5);
    pr=1;
    m=read();
    n=read();
    k=read();
    for(int i=1; i<=m; i++)
    {
        int x=read();
        pr=pr*inv[x]%MOD;
        for(int j=0; j<x; j++)
        {
            int tmp=C(x,j)*fac[x-1]%MOD*inv[x-j-1]%MOD;
            f[i].push_back(tmp);
        }
        q.push(node{i,x});
    }
    while(q.size()>1)
    {
        int x1=q.top().x,s1=q.top().s;
        q.pop();
        int x2=q.top().x,s2=q.top().s;
        q.pop();
        len=1;
        lg=0;
        while(len<=s1+s2) len<<=1,lg++;
        for(int i=0; i<len; i++) A[i]=B[i]=0;
        for(int i=0; i<s1; i++) A[i]=f[x1][i];
        for(int i=0; i<s2; i++) B[i]=f[x2][i];
        NTT(A,len,1);
        NTT(B,len,1);
        for(int i=0; i<len; i++) A[i]=A[i]*B[i]%MOD;
        NTT(A,len,-1);
        f[x1].clear();
        for(int i=0; i<s1+s2; i++)
            f[x1].push_back(A[i]);
        q.push(node{x1,s1+s2-1});
    }
    int x=q.top().x,s=q.top().s;
    for(int i=k; i<s; i++)
    {
        int fl=(i-k)%2?-1:1,g=f[x][i]*fac[n-i]%MOD;
        ans=((ans+fl*C(i,k)*g)%MOD+MOD)%MOD;
    }
    ans=ans*pr%MOD;
    printf("%lld\n",ans);
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值