[2018.7.4集训]magic-多项式相关-容斥原理

题目大意

有$m$种卡片,共$n$张,第$i$种数量为$a_i$张。
将$n$张卡顺次摆放可以得到一个魔术序列,在魔术序列中,若相邻两张卡种类相同,则它们被称为一个魔术对。
求出本质不同的恰好包含$k$个魔术对的魔术序列数量,对$998244353$取模。

$0 \leq k,n \leq 100000 , 1\leq m \leq 20000,\sum_{i=1}^m a_i=n$

题解

本质不同很难做,那么先假装同种的卡片互不相同。

考虑容斥,设$g_i$表示至少存在$i$个魔术对的序列数。
为了计算这个$g_i$,设$f[i][j]$表示前$i$种卡片至少钦定了$j$个魔术对的方案数。

可以发现,最终每种卡片的方案将会是一个个连续段,且每种方案贡献的对数为$a_i$减去分段数的值。
于是,对于每次转移,钦定当前种的若干张强行接在其他没有被钦定的魔术卡后方来组成一个个段。
这样,剩下的没有被钦定的有可能会师一段序列的开头,随意往原序列里插即可~

于是设当前种选择了$l$张,那么有转移:

$$f[i][j]=f[i-1][j-l]*\binom{a_i}{l}*\frac{(a_i-1)!}{(a_i-l-1)!}$$

由于在dp过程中,咱们相当于只考虑没被钦定的卡片的顺序,再随意插入被钦定的卡,因此现在需要考虑被钦定了的卡的贡献,于是除去一个阶乘,有$g_i=\frac{f[m][i]}{(n-i)!}$。

可以发现,$f[i][j]$的转移是多项式的形式,可以利用堆实现的分治$FFT$优化至$O(n\log^2n)$。

接下来,就需要容斥了。
设$f_i$为恰好有$i$个魔术对的方案数量,那么有一个明显的式子:
$$g_i=\sum_{j=i}^{n}{j \choose i}f_j$$
于是考虑广义容斥原理:
$$f_i=\sum_{j=i}^{n}(-1)^{j-i}{j \choose i}g_j$$

由于最初假设了同种卡片互不相同,那么除去每种卡片自身的排列,有

$$ans=\frac{f_k}{\prod_{i=1}^{m}a_i!}$$

于是收工!

代码:

#include<queue>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;

inline int read()
{
    int x=0;char ch=getchar();
    while(ch<'0' || '9'<ch)ch=getchar();
    while('0'<=ch && ch<='9')x=x*10+(ch^48),ch=getchar();
    return x;
}

typedef long long ll;
typedef pair<int,int> pr;
const int M=20009;
const int N=400009;
const ll md=998244353;

int m,n,k,a[M];
ll fac[N],inv[N],ans;
ll f[N],g[N];
priority_queue<pr,vector<pr>,greater<pr> > q;
vector<int> h[M];

inline ll qpow(ll a,ll b)
{
    ll ret=1;
    while(b)
    {
        if(b&1)ret=ret*a%md;
        a=a*a%md;b>>=1;
    }
    return ret;
}

inline void init()
{
    fac[0]=1;
    for(int i=1;i<N;i++)
        fac[i]=fac[i-1]*i%md;
    inv[N-1]=qpow(fac[N-1],md-2);
    for(int i=N-1;i>=1;i--)
        inv[i-1]=inv[i]*i%md;
}

inline ll c(ll a,ll b)
{
    if(a<b)return 0;
    return fac[a]*inv[b]%md*inv[a-b]%md;
}

namespace ntt
{
    int rev[N];

    inline void ntt(ll *a,int n,int f)
    {
        for(int i=0;i<n;i++)if(i<rev[i])swap(a[i],a[rev[i]]);
        for(int h=2;h<=n;h<<=1)
        {
            ll w=qpow(3,(md-1)/h);
            if(f)w=qpow(w,md-2);
            for(int j=0;j<n;j+=h)
            {
                ll wn=1ll,x,y;
                for(int k=j;k<j+(h>>1);k++)
                {
                    x=a[k];y=a[k+(h>>1)]*wn%md;wn=wn*w%md;
                    a[k]=(x+y)%md;a[k+(h>>1)]=(x-y+md)%md;
                }
            }
        }
        if(f)
            for(ll i=0,invn=qpow(n,md-2);i<n;i++)
                a[i]=a[i]*invn%md;
    }

    inline void initrev(int n)
    {
        for(int i=0;i<n;i++)
            rev[i]=(rev[i>>1]>>1)|((i&1)*(n>>1));
    }

    inline void mul(vector<int> &a,int n,vector<int> &b,int m,vector<int> &c)
    {
        static ll d[N],e[N],l;
        for(l=1;l<=n+m;l<<=1);initrev(l);
        for(int i=0;i<n;i++)d[i]=a[i];
        for(int i=n;i<l;i++)d[i]=0;
        for(int i=0;i<m;i++)e[i]=b[i];
        for(int i=m;i<l;i++)e[i]=0;
        ntt(d,l,0);ntt(e,l,0);
        for(int i=0;i<l;i++)
            d[i]=d[i]*e[i]%md;
        ntt(d,l,1);c.resize(n+m-1);
        for(int i=0;i<n+m-1;i++)
            c[i]=d[i];
    }
}

int main()
{
    init();
    m=read();n=read();k=read();
    for(int i=1;i<=m;i++)
        a[i]=read();

    for(int i=1;i<=m;i++)
    {
        for(int j=0;j<a[i];j++)
            h[i].push_back(c(a[i],j)*fac[a[i]-1]%md*inv[a[i]-j-1]%md);
        q.push(pr(h[i].size(),i));
    }

    for(int t=1;t<=m-1;t++)
    {
        int px=q.top().second;q.pop();
        int py=q.top().second;q.pop();
        ntt::mul(h[px],h[px].size(),h[py],h[py].size(),h[px]);
        q.push(pr(h[px].size(),px));
    }
    
    int lst=q.top().second;q.pop();
    for(int i=0;i<h[lst].size() && i<=n;i++)
        g[i]=h[lst][i]*fac[n-i]%md;

    ll ans=0;
    for(int j=k;j<=n;j++)
        (ans+=(((j-k)&1)?(md-1ll):1ll)*c(j,k)%md*g[j]%md)%=md;
    for(int i=1;i<=m;i++)
        ans=ans*inv[a[i]]%md;
    printf("%lld\n",ans);
    return 0;
}

转载于:https://www.cnblogs.com/zltttt/p/9271521.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值